From fbd7b8de222a2fb1cb99691af51c8ba748417eec Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Wed, 2 Jul 2025 13:55:25 +0400 Subject: [PATCH 1/5] Formula editor parsed to small modules, also added table() function in reports --- formula_editor/api.py | 1095 +-- .../doctype/formula_editor/formula_editor.js | 8681 +---------------- .../doctype/formula_editor/test.js | 0 formula_editor/hooks.py | 2 +- .../public/js/formula_editor_dragdrop.js | 458 + .../public/js/formula_editor_formulas.js | 477 + .../public/js/formula_editor_generator.js | 460 + .../public/js/formula_editor_modals.js | 689 ++ .../public/js/formula_editor_reports.js | 1390 +++ .../public/js/formula_editor_table_rows.js | 1422 +++ .../public/js/formula_editor_triggers.js | 418 + formula_editor/public/js/formula_editor_ui.js | 612 ++ .../public/js/formula_editor_universal.js | 1377 +++ .../public/js/formula_editor_utils.js | 171 + package-lock.json | 1009 ++ package.json | 5 + 16 files changed, 9000 insertions(+), 9266 deletions(-) create mode 100644 formula_editor/formula_editor/doctype/formula_editor/test.js create mode 100644 formula_editor/public/js/formula_editor_dragdrop.js create mode 100644 formula_editor/public/js/formula_editor_formulas.js create mode 100644 formula_editor/public/js/formula_editor_generator.js create mode 100644 formula_editor/public/js/formula_editor_modals.js create mode 100644 formula_editor/public/js/formula_editor_reports.js create mode 100644 formula_editor/public/js/formula_editor_table_rows.js create mode 100644 formula_editor/public/js/formula_editor_triggers.js create mode 100644 formula_editor/public/js/formula_editor_ui.js create mode 100644 formula_editor/public/js/formula_editor_universal.js create mode 100644 formula_editor/public/js/formula_editor_utils.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/formula_editor/api.py b/formula_editor/api.py index 29c5d9c..52ec0a7 100644 --- a/formula_editor/api.py +++ b/formula_editor/api.py @@ -2,17 +2,23 @@ import frappe import json import copy from frappe.model.document import Document -from frappe.utils import getdate, nowdate, get_first_day, get_last_day, add_months, today +from frappe.utils import getdate, nowdate, flt import re @frappe.whitelist() def get_doctype_fields(doctype): + """Получение числовых полей DocType для создания формул""" meta = frappe.get_meta(doctype) fields = [] + # Получаем все числовые поля for field in meta.fields: if field.fieldtype in ["Int", "Float", "Currency"]: - fields.append({"fieldname": field.fieldname, "label": field.label, "fieldtype": field.fieldtype}) + fields.append({ + "fieldname": field.fieldname, + "label": field.label, + "fieldtype": field.fieldtype + }) # Проверяем таблицы внутри DocType for child in meta.fields: @@ -26,9 +32,7 @@ def get_doctype_fields(doctype): "label": f"{child.label} → {sub_field.label}", "fieldtype": sub_field.fieldtype }) - - # Убираем генерацию полей для конкретных строк таблицы - # Теперь пользователь будет использовать селектор + return fields @frappe.whitelist() @@ -83,96 +87,31 @@ def on_trash(doc, method=None): frappe.delete_doc("Client Script", script.name, force=True) frappe.db.commit() except Exception as e: - return + pass @frappe.whitelist() def run_report_for_formula(report_name, filters, field_name=None): - """Выполняет отчет с указанными фильтрами и возвращает результат для формулы""" + """Выполняет кастомный отчет с указанными фильтрами и возвращает результат для формулы""" try: - + # Парсим фильтры, если они переданы как строка if isinstance(filters, str): filters = json.loads(filters) - final_filters = {} - - for key, value in filters.items(): - if not ('from_date' in key or 'to_date' in key or 'from' == key or 'to' == key or 'period_start_date' == key or 'period_end_date' == key): - final_filters[key] = value - - if "company" not in final_filters or not final_filters["company"]: - final_filters["company"] = frappe.defaults.get_user_default("Company") or frappe.db.get_single_value("Global Defaults", "default_company") - - from_date_key = None - to_date_key = None - from_date_value = None - to_date_value = None - - for key, value in filters.items(): - if 'from_date>' in key or 'from_date>=' in key: - from_date_key = key - from_date_value = value - elif 'to_date<' in key or 'to_date<=' in key: - to_date_key = key - to_date_value = value - - if from_date_key and from_date_value: - if isinstance(from_date_value, str): - from_date_value = frappe.utils.getdate(from_date_value) - final_filters['from_date'] = from_date_value - - if to_date_key and to_date_value: - if isinstance(to_date_value, str): - to_date_value = frappe.utils.getdate(to_date_value) - final_filters['to_date'] = to_date_value - - if 'from_date' not in final_filters and 'from_date' in filters: - final_filters['from_date'] = frappe.utils.getdate(filters['from_date']) if isinstance(filters['from_date'], str) else filters['from_date'] - - if 'to_date' not in final_filters and 'to_date' in filters: - final_filters['to_date'] = frappe.utils.getdate(filters['to_date']) if isinstance(filters['to_date'], str) else filters['to_date'] - - if 'from_date' not in final_filters: - final_filters['from_date'] = frappe.utils.get_first_day(frappe.utils.nowdate()) - - if 'to_date' not in final_filters: - final_filters['to_date'] = frappe.utils.nowdate() - - # === Универсальная логика для подстановки доп. полей на основе даты === - has_date_filters = 'from_date' in final_filters or 'to_date' in final_filters - report_name_lc = report_name.lower() - - if has_date_filters: - # Применяем поля from и to для всех отчетов, кроме cash flow - if report_name_lc not in ["cash flow", "cash_flow"]: - final_filters['from'] = final_filters.get('from_date') - final_filters['to'] = final_filters.get('to_date') - # Специальная обработка только для cash flow - elif report_name_lc in ["cash flow", "cash_flow"]: - final_filters['period_start_date'] = final_filters.get('from_date') - final_filters['period_end_date'] = final_filters.get('to_date') - final_filters['filter_based_on'] = "Date Range" - final_filters['periodicity'] = "Monthly" - - # === Универсальное поведение: если есть дата-фильтры — используем их напрямую === - if has_date_filters: - standard_filters = final_filters - custom_filters = {} - else: - standard_filters, custom_filters = generate_dynamic_filters(report_name, final_filters) + # Обрабатываем специальные значения в фильтрах + processed_filters = process_filters_for_custom_reports(filters) + # Выполняем отчет from frappe.desk.query_report import run - report_result = run(report_name, filters=standard_filters) + report_result = run(report_name, filters=processed_filters) + # Проверяем результат на ошибки if isinstance(report_result, dict) and report_result.get('message'): return {"error": report_result.get('message')} columns = report_result.get('columns', []) data = report_result.get('result', []) - - if custom_filters: - data = apply_custom_filters(data, custom_filters, columns) - + # Если указано конкретное поле, извлекаем его значение if field_name: result = extract_field_from_report(data, columns, field_name) return result @@ -181,328 +120,14 @@ def run_report_for_formula(report_name, filters, field_name=None): "columns": columns, "data": data } + except Exception as e: + frappe.log_error(f"Error in run_report_for_formula: {str(e)}") return {"error": str(e)} -@frappe.whitelist() -def generate_dynamic_filters(report_name, user_filters): - """Обрабатывает фильтры для отчета, разделяя их на стандартные и пользовательские""" - - # Создаем копию пользовательских фильтров - processed_filters = copy.deepcopy(user_filters) - - # Преобразуем даты в datetime объекты - date_fields = [ - "from_date", "to_date", "date", - "period_start_date", "period_end_date", - "year_start_date", "year_end_date" - ] - - for field in date_fields: - if field in processed_filters and isinstance(processed_filters[field], str): - try: - processed_filters[field] = frappe.utils.getdate(processed_filters[field]) - except: - # Если не удаётся преобразовать, оставляем как есть - pass - - # ИСПРАВЛЕНИЕ: для отчета General Ledger всегда используем фильтры как стандартные - if report_name == "General Ledger": - return processed_filters, {} - - # Для всех остальных отчетов используем стандартный механизм - standard_filters = {} - custom_filters = {} - - try: - from frappe.desk.query_report import get_script - script_data = get_script(report_name) - report_filters = script_data.get('filters') or [] - - # Определяем, какие фильтры являются стандартными - standard_filter_names = [] - for f in report_filters: - if isinstance(f, dict): - standard_filter_names.append(f.get('fieldname')) - elif isinstance(f, list) and len(f) > 0: - standard_filter_names.append(f[0]) - - # Разделяем фильтры на стандартные и пользовательские - for key, value in processed_filters.items(): - if key in standard_filter_names: - standard_filters[key] = value - else: - # Проверяем "All" значения в фильтрах - if isinstance(value, str) and "All " in value: - # Игнорируем универсальные значения типа "All Item Groups" - field_base = key.replace('_group', '').replace('_category', '') - if value == f"All {field_base.title()}s" or value == f"All {key.title()}s": - continue - custom_filters[key] = value - except Exception as e: - # В случае ошибки, считаем все фильтры стандартными - standard_filters = processed_filters - custom_filters = {} - - return standard_filters, custom_filters - -@frappe.whitelist() -def apply_custom_filters(data, custom_filters, columns): - """Применяет пользовательские фильтры к результатам отчета""" - filtered_data = [] - - # Для каждой строки результата проверяем соответствие пользовательским фильтрам - for row in data: - match = True - - for filter_name, filter_value in custom_filters.items(): - # Проверяем, есть ли поле в строке - if isinstance(row, dict) and filter_name in row: - # Если значение фильтра не совпадает со значением в строке, исключаем строку - row_value = str(row[filter_name]).lower() if row[filter_name] is not None else "" - filter_str = str(filter_value).lower() - if filter_str != row_value: - match = False - break - elif not isinstance(row, dict): - # Для строк в виде списков ищем колонку по имени - column_idx = None - for i, col in enumerate(columns): - if isinstance(col, dict) and col.get('fieldname') == filter_name: - column_idx = i - break - elif isinstance(col, str) and filter_name in col.lower(): - column_idx = i - break - - if column_idx is not None and column_idx < len(row): - row_value = str(row[column_idx]).lower() if row[column_idx] is not None else "" - filter_str = str(filter_value).lower() - if filter_str != row_value: - match = False - break - - # Если строка соответствует всем фильтрам, добавляем её в результат - if match: - filtered_data.append(row) - - return filtered_data - -@frappe.whitelist() -def extract_field_from_report(data, columns, field_name): - """Извлекает значение конкретного поля из результатов отчета""" - - if not data: - return 0 - - # Определяем индекс колонки по имени поля - column_idx = None - for i, col in enumerate(columns): - if isinstance(col, dict): - if col.get('fieldname') == field_name or col.get('label') == field_name: - column_idx = i - break - elif isinstance(col, str) and (col == field_name or field_name.lower() in col.lower()): - column_idx = i - break - - # Если колонка не найдена, ищем ее по любым подстрокам в имени - if column_idx is None: - for i, col in enumerate(columns): - col_name = "" - if isinstance(col, dict): - col_name = col.get('fieldname', '') or col.get('label', '') - elif isinstance(col, str): - col_name = col - - # Проверка на частичное совпадение - if field_name.lower() in col_name.lower() or "balance" in col_name.lower(): - column_idx = i - break - - # Если колонка найдена, сначала ищем итоговую строку - if column_idx is not None: - # Определяем возможные маркеры итоговых строк - total_markers = ['total', 'итого', 'grand total', 'общий итог', 'итого по', 'total row'] - - # Ищем итоговые строки - total_rows = [] - - for idx, row in enumerate(data): - is_total_row = False - - if isinstance(row, dict): - # Проверяем ключи и значения на наличие маркеров итога - for key, value in row.items(): - if isinstance(value, str) and any(marker in value.lower() for marker in total_markers): - is_total_row = True - break - if isinstance(key, str) and any(marker in key.lower() for marker in total_markers): - is_total_row = True - break - - # Проверяем специальные атрибуты итоговых строк - if (row.get('is_total_row') or - row.get('bold') or - row.get('total_row') or - row.get('is_group', 0) == 0 and row.get('indent', 0) == 0): - is_total_row = True - - elif isinstance(row, list) and len(row) > 0: - # Для списков проверяем первый элемент - first_element = str(row[0]) if row[0] is not None else "" - if any(marker in first_element.lower() for marker in total_markers): - is_total_row = True - - if is_total_row: - total_rows.append((idx, row)) - - # Если нашли итоговые строки, используем последнюю (обычно она Grand Total) - if total_rows: - # Сортируем по индексу в обратном порядке, чтобы получить последнюю итоговую строку - total_rows.sort(key=lambda x: x[0], reverse=True) - _, total_row = total_rows[0] - - # Извлекаем значение из итоговой строки - if isinstance(total_row, dict): - if field_name in total_row: - value = total_row[field_name] - else: - # Поиск с игнорированием регистра - for key in total_row: - if key.lower() == field_name.lower() or ( - field_name.lower() in key.lower() and - any(num_word in key.lower() for num_word in ['amount', 'total', 'sum', 'balance']) - ): - value = total_row[key] - break - else: - value = None - elif isinstance(total_row, list) and column_idx < len(total_row): - value = total_row[column_idx] - else: - value = None - - # Преобразуем в число - try: - if value is not None: - return float(value) - except (ValueError, TypeError): - pass - - # Если итоговую строку не нашли, проверяем последнюю строку - # (часто это итоговая строка без явного маркера) - if len(data) > 0: - last_row = data[-1] - - # Извлекаем значение из последней строки - if isinstance(last_row, dict): - if field_name in last_row: - value = last_row[field_name] - else: - # Поиск с игнорированием регистра - for key in last_row: - if key.lower() == field_name.lower() or ( - field_name.lower() in key.lower() and - any(num_word in key.lower() for num_word in ['amount', 'total', 'sum', 'balance']) - ): - value = last_row[key] - break - else: - value = None - elif isinstance(last_row, list) and column_idx < len(last_row): - value = last_row[column_idx] - else: - value = None - - # Проверяем, похоже ли это на итоговую строку - # (по сравнению с предпоследней строкой) - try: - if value is not None and len(data) > 1: - second_last_row = data[-2] - second_last_value = None - - if isinstance(second_last_row, dict) and field_name in second_last_row: - second_last_value = second_last_row[field_name] - elif not isinstance(second_last_row, dict) and column_idx < len(second_last_row): - second_last_value = second_last_row[column_idx] - - # Если последнее значение существенно больше предыдущего, - # вероятно это строка итога - if (second_last_value is not None and - isinstance(value, (int, float)) and - isinstance(second_last_value, (int, float)) and - value > second_last_value * 1.5): - return float(value) - except (ValueError, TypeError, IndexError): - pass - - # Если ни одна из проверок не сработала, суммируем все значения как раньше - total = 0 - count = 0 - - for row in data: - 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: - # Поиск поля с игнорированием регистра - if isinstance(row, dict): - found = False - for key in row: - if key.lower() == field_name.lower() or "balance" in key.lower(): - value = row[key] - found = True - break - if not found: - continue - else: - continue - - # Преобразуем в число - try: - if value is not None: - num_value = float(value) - total += num_value - count += 1 - except (ValueError, TypeError): - pass - - return total - else: - # Если колонка не найдена, ищем любое первое числовое поле - total = 0 - count = 0 - - for idx, row in enumerate(data[:5]): - if isinstance(row, dict): - for key, value in row.items(): - try: - if value is not None: - num_value = float(value) - total += num_value - count += 1 - break - except (ValueError, TypeError): - continue - elif isinstance(row, list): - for i, value in enumerate(row): - try: - if value is not None: - num_value = float(value) - total += num_value - count += 1 - break - except (ValueError, TypeError): - continue - - # Возвращаем итоговую сумму или 0 - return total - @frappe.whitelist() def get_report_fields(report_name, filters=None): - """Получает поля из отчета для использования в Formula Editor""" + """Получает поля из кастомного отчета для использования в Formula Editor""" if not report_name: frappe.throw("Не указано имя отчета") @@ -521,74 +146,16 @@ def get_report_fields(report_name, filters=None): if report_doc.report_type != "Script Report": frappe.throw("Выбранный отчет должен быть типа 'Script Report'") - # Специальная обработка для отчета Cash Flow - if report_name.lower() in ["cash flow", "cash_flow"]: - # Предопределенные поля для отчета Cash Flow - return [ - {"fieldname": "total_cash", "label": "Total Cash", "fieldtype": "Currency"}, - {"fieldname": "net_cash", "label": "Net Cash", "fieldtype": "Currency"}, - {"fieldname": "opening_balance", "label": "Opening Balance", "fieldtype": "Currency"}, - {"fieldname": "closing_balance", "label": "Closing Balance", "fieldtype": "Currency"}, - {"fieldname": "inflow", "label": "Cash Inflow", "fieldtype": "Currency"}, - {"fieldname": "outflow", "label": "Cash Outflow", "fieldtype": "Currency"} - ] - - # Специальная обработка для отчета General Ledger - if report_name.lower() in ["general ledger", "general_ledger"]: - return [ - {"fieldname": "debit", "label": "Debit", "fieldtype": "Currency"}, - {"fieldname": "credit", "label": "Credit", "fieldtype": "Currency"}, - {"fieldname": "balance", "label": "Balance", "fieldtype": "Currency"}, - {"fieldname": "account", "label": "Account", "fieldtype": "Link"}, - {"fieldname": "party_type", "label": "Party Type", "fieldtype": "Link"}, - {"fieldname": "party", "label": "Party", "fieldtype": "Dynamic Link"}, - {"fieldname": "voucher_type", "label": "Voucher Type", "fieldtype": "Link"}, - {"fieldname": "voucher_no", "label": "Voucher No", "fieldtype": "Dynamic Link"}, - {"fieldname": "against_account", "label": "Against Account", "fieldtype": "Link"}, - {"fieldname": "debit_in_account_currency", "label": "Debit in Account Currency", "fieldtype": "Currency"}, - {"fieldname": "credit_in_account_currency", "label": "Credit in Account Currency", "fieldtype": "Currency"}, - {"fieldname": "balance_in_account_currency", "label": "Balance in Account Currency", "fieldtype": "Currency"} - ] - - # Для остальных отчетов - стандартная обработка + # Подготавливаем базовые фильтры + base_filters = prepare_base_filters(filters) + + # Выполняем отчет для получения колонок from frappe.desk.query_report import run - - # Добавляем фильтр компании, если его нет, но он требуется - if filters is None: - filters = {} - - if 'company' not in filters: - default_company = frappe.defaults.get_user_default("Company") or frappe.db.get_single_value("Global Defaults", "default_company") - if default_company: - filters['company'] = default_company - - # Добавляем фильтры дат, если их нет - if 'from_date' not in filters: - filters['from_date'] = frappe.utils.get_first_day(frappe.utils.nowdate()) - if 'to_date' not in filters: - filters['to_date'] = frappe.utils.nowdate() - - # Используем базовые фильтры или переданные пользователем - base_filters = filters or { - "from_date": frappe.utils.get_first_day(frappe.utils.nowdate()), - "to_date": frappe.utils.nowdate() - } - - # Преобразуем строки в объекты datetime.date - for key in ["from_date", "to_date"]: - if key in base_filters and isinstance(base_filters[key], str): - try: - base_filters[key] = getdate(base_filters[key]) - except Exception: - pass - - # Выполняем запрос отчета для получения колонок try: result = run(report_name, filters=base_filters) except Exception as e: frappe.throw(f"Ошибка при выполнении отчета: {str(e)}") - if not result: frappe.throw("Отчет не вернул результатов") @@ -597,7 +164,6 @@ def get_report_fields(report_name, filters=None): # Получаем колонки отчета columns = result.get('columns', []) - fields = [] # Преобразуем колонки в удобный формат @@ -619,39 +185,16 @@ def get_report_fields(report_name, filters=None): } # Добавляем только числовые поля и поля, которые могут быть преобразованы в числа - if field_data.get('fieldtype') in ['Int', 'Float', 'Currency', 'Percent'] or \ - any(numeric_term in field_data.get('label', '').lower() for numeric_term in ['amount', 'total', 'sum', 'qty', 'price', 'rate', 'number']): + if is_numeric_field(field_data): fields.append(field_data) + # Если нет явно числовых полей, анализируем данные if not fields: - # Анализируем данные для поиска числовых полей, даже если они не отмечены как числовые data = result.get('result', []) - 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)) and key not in [f['fieldname'] for f in fields]: - 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}") - - if col_name not in [f['fieldname'] for f in fields]: - 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" - }) + fields = find_numeric_fields_in_data(data, columns) if not fields: - frappe.throw(f"В отчете не найдено числовых полей. Пожалуйста, выберите другой отчет или свяжитесь с администратором.") + frappe.throw(f"В отчете не найдено числовых полей. Пожалуйста, убедитесь, что в отчете есть поля с типом Currency, Float или Int.") return fields @@ -660,14 +203,13 @@ def get_report_fields(report_name, filters=None): @frappe.whitelist() def get_report_filters_dynamic(report_name): - """Получить доступные фильтры для указанного отчета в формате для динамического создания полей""" + """Получить доступные фильтры для указанного кастомного отчета""" if not report_name: return [] try: # Получаем документ отчета - from frappe.desk.query_report import get_report_doc, get_script - report_doc = get_report_doc(report_name) + report_doc = frappe.get_doc("Report", report_name) # Проверяем, что отчет - script report if report_doc.report_type != "Script Report": @@ -705,128 +247,479 @@ def get_report_filters_dynamic(report_name): } formatted_filters.append(filter_data) - # Если фильтров нет в метаданных, пробуем запустить отчет с минимальными фильтрами + # Если фильтров нет в метаданных, добавляем базовые if not formatted_filters: - # Пытаемся запустить отчет с минимальными фильтрами - # Базовые фильтры, которые часто требуются отчетам - base_filters = { - "from_date": frappe.utils.get_first_day(frappe.utils.nowdate()), - "to_date": frappe.utils.nowdate() - } - - # Используем безопасный способ выполнения отчета через API - from frappe.desk.query_report import run - try: - result = run(report_name, filters=base_filters) - - # Если отчет выполнился успешно, используем базовые фильтры - if isinstance(result, dict) and not result.get('message'): - # Добавляем базовые фильтры, которые использовались для успешного запуска - for key, value in base_filters.items(): - fieldtype = 'Date' if 'date' in key.lower() else 'Data' - - formatted_filters.append({ - 'fieldname': key, - 'label': key.replace('_', ' ').title(), - 'fieldtype': fieldtype, - 'default': value, - 'reqd': 1 # Считаем базовые фильтры обязательными - }) - except Exception as e: - - # Анализируем ошибку на предмет недостающих фильтров - error_msg = str(e) - missing_filter_names = extract_filter_names_from_error(error_msg) - - # Добавляем базовые фильтры - for key, value in base_filters.items(): - fieldtype = 'Date' if 'date' in key.lower() else 'Data' - - formatted_filters.append({ - 'fieldname': key, - 'label': key.replace('_', ' ').title(), - 'fieldtype': fieldtype, - 'default': value, - 'reqd': 1 if key in missing_filter_names else 0 - }) - - # Добавляем фильтры, упомянутые в сообщении об ошибке - for filter_name in missing_filter_names: - if not any(f['fieldname'] == filter_name for f in formatted_filters): - filter_data = { - 'fieldname': filter_name, - 'label': filter_name.replace('_', ' ').title(), - 'fieldtype': 'Data', - 'reqd': 1 - } - - # Предположения о типе поля на основе названия - if 'date' in filter_name.lower(): - filter_data['fieldtype'] = 'Date' - elif 'company' in filter_name.lower(): - filter_data['fieldtype'] = 'Link' - filter_data['options'] = 'Company' - elif 'item' == filter_name.lower(): - filter_data['fieldtype'] = 'Link' - filter_data['options'] = 'Item' - elif 'group' in filter_name.lower() and 'item' in filter_name.lower(): - filter_data['fieldtype'] = 'Link' - filter_data['options'] = 'Item Group' - - formatted_filters.append(filter_data) + formatted_filters = get_default_filters() - # Получаем дефолтную компанию - default_company = frappe.defaults.get_user_default("Company") or frappe.db.get_single_value("Global Defaults", "default_company") - - # Находим и обновляем фильтр компании или добавляем его, если отсутствует - company_filter = next((f for f in formatted_filters if f['fieldname'] == 'company'), None) - - if company_filter: - company_filter['default'] = default_company - company_filter['hidden'] = 1 # Скрываем фильтр компании всегда - company_filter['reqd'] = 0 # Важно! Не делаем его обязательным для пользовательского ввода - else: - # Добавляем скрытый фильтр компании, если его нет - formatted_filters.append({ - 'fieldname': 'company', - 'label': 'Company', - 'fieldtype': 'Link', - 'options': 'Company', - 'default': default_company, - 'reqd': 0, # Не делаем обязательным - 'hidden': 1 # Скрываем фильтр компании всегда - }) + # Обеспечиваем наличие базовых фильтров + ensure_basic_filters(formatted_filters) return formatted_filters except Exception as e: - return [] + 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 extract_filter_names_from_error(error_msg): - """Извлечь имена отсутствующих фильтров из сообщения об ошибке""" - filter_names = set() +def prepare_base_filters(filters): + """Подготавливает базовые фильтры с значениями по умолчанию""" + if not filters: + filters = {} - # Типичные шаблоны ошибок в отчетах Frappe - import re + # Автоматическая подстановка компании + if not filters.get("company"): + filters["company"] = ( + frappe.defaults.get_user_default("Company") or + frappe.db.get_single_value("Global Defaults", "default_company") + ) - # Например: "Mandatory filters required: company, from_date" - mandatory_pattern = re.search(r'Mandatory\s+filters\s+required\s*:\s*(.*)', error_msg, re.IGNORECASE) - if mandatory_pattern: - filter_list = mandatory_pattern.group(1).strip() - for filter_name in filter_list.split(','): - filter_name = filter_name.strip() - if filter_name: - filter_names.add(filter_name) + # Автоматическая подстановка дат + if not filters.get("from_date"): + filters["from_date"] = frappe.utils.get_first_day(frappe.utils.nowdate()) - # Например: "Filter 'company' is mandatory" - single_filter_pattern = re.findall(r'[Ff]ilter\s+[\'"]([^\'"]+)[\'"]', error_msg) - for filter_name in single_filter_pattern: - filter_names.add(filter_name) + if not filters.get("to_date"): + filters["to_date"] = frappe.utils.nowdate() - # Например: "Missing parameter: company" - missing_param_pattern = re.findall(r'[Mm]issing\s+parameter\s*:\s*([^\s,]+)', error_msg) - for filter_name in missing_param_pattern: - filter_names.add(filter_name) + # Преобразование строковых дат в объекты дат + 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 list(filter_names) + 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 + + # Определяем индекс колонки по имени поля + column_idx = None + for i, col in enumerate(columns): + if isinstance(col, dict): + if col.get('fieldname') == field_name or col.get('label') == field_name: + column_idx = i + break + elif isinstance(col, str) and (col == field_name or field_name.lower() in col.lower()): + column_idx = i + break + + # Если колонка не найдена, ищем ее по любым подстрокам в имени + if column_idx is None: + for i, col in enumerate(columns): + col_name = "" + if isinstance(col, dict): + col_name = col.get('fieldname', '') or col.get('label', '') + 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 + + if isinstance(row, dict): + # Проверяем специальные атрибуты итоговых строк + if (row.get('is_total_row') or + row.get('bold') or + row.get('total_row')): + is_total_row = True + + # Проверяем ключи и значения на наличие маркеров итога + if not is_total_row: + for key, value in row.items(): + if isinstance(value, str) and any(marker in value.lower() for marker in total_markers): + is_total_row = True + break + + elif isinstance(row, list) and len(row) > 0: + # Для списков проверяем первый элемент + first_element = str(row[0]) if row[0] is not None else "" + if any(marker in first_element.lower() for marker in total_markers): + is_total_row = True + + # Если нашли итоговую строку, извлекаем значение + if is_total_row: + if isinstance(row, dict): + if field_name in row: + value = row[field_name] + else: + # Поиск с игнорированием регистра + value = None + 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] + else: + value = None + + # Преобразуем в число + try: + if value is not None: + return float(value) + except (ValueError, TypeError): + pass + + # Если итоговую строку не нашли, суммируем все значения + total = 0 + count = 0 + + for row in data: + 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: + # Поиск поля с игнорированием регистра + 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 + + # Преобразуем в число + try: + if value is not None: + num_value = float(value) + total += num_value + count += 1 + except (ValueError, TypeError): + pass + + 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 + }, + { + '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] + + # Добавляем компанию, если её нет + 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 + }) + +# Генерируем функцию для работы с отчетами для клиентского скрипта +def generateReportFieldsFunction(report_fields): + """Генерирует JavaScript функцию для работы с полями отчетов""" + if not report_fields: + return "" + + return f""" +window.notifiedAboutAyIl = false; +window.notifiedAboutRubIl = false; + +async function getReportFieldValue(frm, reportFieldName) {{ + try {{ + // Получаем информацию о поле отчета + const reportFields = {json.dumps(report_fields)}; + const fieldInfo = reportFields[reportFieldName]; + + if (!fieldInfo) {{ + return 0; + }} + + // Парсим фильтры + let filters = {{}}; + try {{ + filters = JSON.parse(fieldInfo.filters_json || '{{}}'); + }} catch (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); + if (frm.doc[docField] !== undefined) {{ + processedFilters[key] = frm.doc[docField]; + continue; + }} + }} + + // Обычные значения + processedFilters[key] = value; + }} + + // Проверяем кеш + if (!window.reportCache) window.reportCache = {{}}; + + const cacheKey = `${{fieldInfo.report}}|${{JSON.stringify(processedFilters)}}|${{fieldInfo.field_name}}`; + const now = new Date().getTime(); + + if (window.reportCache[cacheKey] && + window.reportCache[cacheKey].timestamp > now - 5 * 60 * 1000) {{ + return window.reportCache[cacheKey].value; + }} + + // Выполняем запрос к серверу + try {{ + const result = await frappe.call({{ + method: "formula_editor.api.run_report_for_formula", + args: {{ + report_name: fieldInfo.report, + filters: processedFilters, + field_name: fieldInfo.field_name + }}, + freeze: false + }}); + + if (result && result.message) {{ + if (result.message.error) {{ + return 0; + }} + + let value = parseFloat(result.message); + if (isNaN(value)) value = 0; + + // Сохраняем в кеш + window.reportCache[cacheKey] = {{ + value: value, + timestamp: now + }}; + + return value; + }} + }} catch (err) {{ + console.error('Report field error:', err); + }} + + return 0; + }} catch (error) {{ + console.error('getReportFieldValue error:', error); + return 0; + }} +}} +""" \ No newline at end of file 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 e80ce32..48cd83b 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,16 @@ +frappe.require([ + "/assets/formula_editor/js/formula_editor_utils.js", + "/assets/formula_editor/js/formula_editor_ui.js", + "/assets/formula_editor/js/formula_editor_formulas.js", + "/assets/formula_editor/js/formula_editor_triggers.js", + "/assets/formula_editor/js/formula_editor_universal.js", + "/assets/formula_editor/js/formula_editor_reports.js", + "/assets/formula_editor/js/formula_editor_table_rows.js", + "/assets/formula_editor/js/formula_editor_dragdrop.js", + "/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) { // Инициализация хранилища для универсальных полей @@ -48,7 +61,7 @@ frappe.ui.form.on('Formula Editor', { } }); -// Управление формулами +// Восстановление формул из хранилища function restore_formulas(frm) { let stored_formulas = JSON.parse(frm.doc.formula_storage || "{}"); @@ -89,6 +102,7 @@ function restore_formulas(frm) { } } +// Сохранение формул в хранилище function save_formulas(frm) { let formula_data = {}; @@ -114,6 +128,7 @@ function save_formulas(frm) { frm.set_value("formula_storage", JSON.stringify(formula_data)); } +// Генерация клиентского скрипта function generate_client_script(frm) { if (!frm.doc.target_doctype) { return; @@ -547,1052 +562,6 @@ function generate_client_script(frm) { update_or_create_client_script(target_doctype, final_script); } -function generateReportFieldsFunction(report_fields) { - - - return ` - window.notifiedAboutAyIl = false; - window.notifiedAboutRubIl = false; -async function getReportFieldValue(frm, reportName, filters, fieldName, aggregation = 'sum') { - try { - - // Проверка необходимости полей сразу для всех фильтров - if (Object.values(filters).some(v => v === "$AyIlSpecial")) { - if (!frm.doc["ay"] || !frm.doc["il"]) { - if (!notifiedAboutAyIl) { - frappe.msgprint({ - title: "Not enough data", - message: "Please fill in the ay and il fields", - indicator: "orange" - }); - notifiedAboutAyIl = true; - } - return 0; - } - } - - // Проверка: если используется $RubIlSpecial, но rüb или il не заполнены — возвращаем 0 - if (Object.values(filters).some(v => v === "$RubIlSpecial")) { - if (!frm.doc["rüb"] || !frm.doc["il"]) { - if (!notifiedAboutRubIl) { - frappe.msgprint({ - title: "Not enough data", - message: "Please fill in the rüb and il fields", - indicator: "orange" - }); - notifiedAboutRubIl = true; - } - return 0; - } - } - - // Обрабатываем фильтры с возможными ссылками на поля документа - const processedFilters = {}; - - // Используем for...in loop для перебора свойств объекта filters - for (const key in filters) { - let value = filters[key]; - - // Специальная обработка для $DefaultCompany - if (value === "$DefaultCompany") { - processedFilters[key] = frm.doc.company || frappe.defaults.get_user_default("Company") || ""; - continue; - } - - // Специальная обработка для $AyIlSpecial - if (value === "$AyIlSpecial") { - // Проверяем, есть ли у документа поля ay и il - if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { - // Обрабатываем специальные поля ay и il для получения диапазона дат - const dateRange = processAzerbayjanDateField({ - ay: frm.doc["ay"], - il: frm.doc["il"] - }); - - // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия - if (dateRange) { - if (key.includes('from_date')) { - // Для начальной даты используем только >= - processedFilters[key] = dateRange.start; - } else if (key.includes('to_date')) { - // Для конечной даты используем только <= - processedFilters[key] = dateRange.end; - } else { - // Для других полей даты используем начало периода - processedFilters[key] = dateRange.start; - } - continue; - } - } - } - - // Специальная обработка для $RubIlSpecial - if (value === "$RubIlSpecial") { - // Проверяем, есть ли у документа поля rüb и il - if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { - // Обрабатываем специальные поля rüb и il для получения диапазона дат - const dateRange = processQuarterDateField({ - rüb: frm.doc["rüb"], - il: frm.doc["il"] - }); - - // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия - if (dateRange) { - if (key.includes('from_date')) { - // Для начальной даты используем только >= - processedFilters[key] = dateRange.start; - } else if (key.includes('to_date')) { - // Для конечной даты используем только <= - processedFilters[key] = dateRange.end; - } else { - // Для других полей даты используем начало периода - processedFilters[key] = dateRange.start; - } - continue; - } - } - } - - // Проверяем, является ли значение ссылкой на поле документа - if (typeof value === 'string' && value.startsWith('doc.')) { - const docField = value.substring(4); // Убираем 'doc.' - - // Проверяем, является ли это специальным полем ay или il - if (docField === 'ay' || docField === 'il') { - // Получаем значения ay и il из документа - if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { - // Обрабатываем специальные поля ay и il для получения диапазона дат - const dateRange = processAzerbayjanDateField({ - ay: frm.doc["ay"], - il: frm.doc["il"] - }); - - // Если удалось получить диапазон дат, создаем правильные фильтры - if (dateRange) { - if (key.includes('from_date')) { - // Для начальной даты используем только >= - processedFilters[key] = dateRange.start; - } else if (key.includes('to_date')) { - // Для конечной даты используем только <= - processedFilters[key] = dateRange.end; - } else { - // Для других полей даты используем начало периода - processedFilters[key] = dateRange.start; - } - continue; - } - } - } - // Проверяем, является ли это специальным полем rüb - else if (docField === 'rüb') { - // Получаем значения rüb и il из документа - if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { - // Обрабатываем специальные поля rüb и il для получения диапазона дат - const dateRange = processQuarterDateField({ - rüb: frm.doc["rüb"], - il: frm.doc["il"] - }); - - // Если удалось получить диапазон дат, создаем правильные фильтры - if (dateRange) { - if (key.includes('from_date')) { - // Для начальной даты используем только >= - processedFilters[key] = dateRange.start; - } else if (key.includes('to_date')) { - // Для конечной даты используем только <= - processedFilters[key] = dateRange.end; - } else { - // Для других полей даты используем начало периода - processedFilters[key] = dateRange.start; - } - continue; - } - } - } - // Для других полей документа - else if (frm.doc[docField] !== undefined) { - // Берем значение из документа - processedFilters[key] = frm.doc[docField]; - continue; - } else { - // Если поле не найдено, пропускаем фильтр - continue; - } - } - - // Добавляем обработанное значение в фильтры - processedFilters[key] = value; - } - - // Проверяем кеш для экономии запросов - if (!window.reportCache) window.reportCache = {}; - - const cacheKey = \`\${reportName}|\${JSON.stringify(processedFilters)}|\${fieldName}|\${aggregation}\`; - - // Если есть в кеше и кеш не устарел (5 минут) - const now = new Date().getTime(); - if (window.reportCache[cacheKey] && - window.reportCache[cacheKey].timestamp > now - 5 * 60 * 1000) { - return window.reportCache[cacheKey].value; - } - - // Выполняем запрос к серверу с обработанными фильтрами - try { - const result = await frappe.call({ - method: "formula_editor.api.run_report_for_formula", - args: { - report_name: reportName, - filters: processedFilters, - field_name: fieldName - }, - freeze: false // Не блокируем интерфейс - }); - - // Проверяем результат - if (result && result.message) { - if (result.message.error) { - return 0; - } - - // Получаем числовое значение - let value = parseFloat(result.message); - if (isNaN(value)) value = 0; - - // Сохраняем в кеш - window.reportCache[cacheKey] = { - value: value, - timestamp: now - }; - - return value; - } - } catch (err) { - } - - return 0; - } catch (error) { - return 0; - } -}`; -} - -// Новая функция для генерации скриптов дочерних таблиц с расширенным логированием -function generate_child_table_script(table_child_scripts, table_doctypes, table_field_triggers, formula_data, functions) { - let child_scripts = ''; - - Object.keys(table_child_scripts).forEach(tableName => { - let childDoctype = table_child_scripts[tableName].doctype; - let usesUniversalFields = table_child_scripts[tableName].uses_universal; - let table_script = `\n\nfrappe.ui.form.on("${childDoctype}", {\n`; - - // Создаем общую функцию для расчета формул в строке таблицы - let calcFunction = ` - async function calculate_${tableName}_row(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - - try { - ${usesUniversalFields ? 'await calculate_universal_fields(frm);' : ''} - - const { transformed, needsAsync, reportCalls } = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}"); - - let finalExpression = transformed; - - if (reportCalls && reportCalls.length > 0) { - const reportResults = []; - - for (const call of reportCalls) { - const result = await getReportFieldValue( - frm, - call.reportName, - call.filters, - call.fieldName, - call.aggregation - ); - reportResults.push({ placeholder: call.placeholder, value: result }); - } - - for (const result of reportResults) { - finalExpression = finalExpression.replace(result.placeholder, result.value); - } - } - - let result = 0; - try { - result = eval(finalExpression); - } catch (e) { - } - - // Обновляем значение - row["${calc.field}"] = isNaN(result) ? 0 : result; - frm.refresh_field("${tableName}"); - } catch (error) { - } - }`; - - table_child_scripts[tableName].calculations.forEach(calc => { - calcFunction += ` - // Расчет для поля ${calc.field} - try { - const transformed = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}").transformed; - row["${calc.field}"] = eval(transformed); - } catch (e) { - row["${calc.field}"] = 0; - } - `; - }); - - calcFunction += ` - frm.refresh_field("${tableName}"); - } catch (e) { - } - }`; - - functions.push(calcFunction); - - // Добавляем обработчики для каждого триггерного поля - Array.from(table_child_scripts[tableName].fields).forEach(field => { - if (usesUniversalFields) { - table_script += ` "${field}": async function(frm, cdt, cdn) { await calculate_${tableName}_row(frm, cdt, cdn); },\n`; - } else { - table_script += ` "${field}": function(frm, cdt, cdn) { calculate_${tableName}_row(frm, cdt, cdn); },\n`; - } - }); - - table_script += `});`; - child_scripts += table_script; - }); - - return child_scripts; -} - -// Вспомогательные функции для generate_client_script -function getTableDoctypes(target_doctype, table_doctypes) { - // Пытаемся получить информацию о таблицах через API - - frappe.call({ - method: "frappe.client.get", - args: { - doctype: "DocType", - name: target_doctype - }, - async: false, - callback: function(r) { - if (r.message && r.message.fields) { - r.message.fields.forEach(field => { - if (field.fieldtype === 'Table' && field.options) { - table_doctypes[field.fieldname] = field.options; - } - }); - } - } - }); - - // Если данные не получены, используем альтернативный способ через meta - if (Object.keys(table_doctypes).length === 0) { - - const meta = frappe.get_meta(target_doctype); - if (meta && meta.fields) { - meta.fields.forEach(field => { - if (field.fieldtype === 'Table' && field.options) { - table_doctypes[field.fieldname] = field.options; - } - }); - } - } - - // Проверяем результат - if (Object.keys(table_doctypes).length === 0) { - } else { - } -} - -function collectChildTables(formula_data, child_tables) { - Object.keys(formula_data).forEach(fieldname => { - let field_info = formula_data[fieldname]; - - if (field_info && field_info.is_table_field) { - // Извлекаем имя табличной части из fieldname (формат: tablename_fieldname) - let parts = fieldname.split('_'); - if (parts.length >= 2) { - let table_name = parts[0]; - if (!child_tables[table_name]) { - child_tables[table_name] = []; - } - child_tables[table_name].push(fieldname); - } - } - }); -} - -function generateUniversalFieldsFunction(universal_fields) { - let universalFieldsCode = - ` - // Функция для расчета универсальных полей - async function calculate_universal_fields(frm) { - ${Object.keys(universal_fields).map(fieldId => { - const field = universal_fields[fieldId]; - if (!field || !field.name || !field.doctype || !field.fieldname) { - return ''; - } - - // Обработка фильтров для ссылок на поля документа - const processedFilters = (field.filters || []).map(filter => { - let newFilter = {...filter}; - - // Проверяем, является ли значение ссылкой на поле документа - if (typeof newFilter.value === 'string' && newFilter.value.startsWith('doc.')) { - const docField = newFilter.value.substring(4); // Убираем 'doc.' - newFilter.valueType = 'field'; - newFilter.docField = docField; - - // Проверяем, является ли это специальным полем ay, il или rüb - newFilter.__is_special_ay_field = (docField === 'ay' || docField === 'il'); - newFilter.__is_special_rub_field = (docField === 'rüb' || docField === 'il'); - } else { - newFilter.valueType = 'static'; - } - return newFilter; - }); - - return ` - // Расчет универсального поля: ${field.name} - try { - let univ_${fieldId} = 0; - // Создаем массив фильтров - let filters_${fieldId} = []; - - // Обрабатываем фильтры с учетом возможных ссылок на поля документа - let skipCalculation_${fieldId} = false; - - ${processedFilters.map(filter => { - if (filter.valueType === 'field') { - if (filter.__is_special_ay_field) { - return ` - // Фильтр со ссылкой на специальное поле месяца (ay) или года (il) - if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { - // Получаем значения месяца и года - const docValues = { - ay: frm.doc["ay"], - il: frm.doc["il"] - }; - - // Проверяем, что поле ay имеет допустимое значение - if (!docValues.ay || typeof docValues.ay !== 'string') { - skipCalculation_${fieldId} = true; - return; - } - - // Проверяем, что поле il имеет допустимое значение - if (!docValues.il || isNaN(parseInt(docValues.il))) { - skipCalculation_${fieldId} = true; - return; - } - - // Обрабатываем специальные поля ay и il для получения диапазона дат - const dateRange = processAzerbayjanDateField(docValues); - - if (dateRange) { - // Добавляем два фильтра для диапазона дат - if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === ">=")) - filters_${fieldId}.push(["${filter.field}", ">=", dateRange.start]); - if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === "<")) - filters_${fieldId}.push(["${filter.field}", "<=", dateRange.end]); - } else { - skipCalculation_${fieldId} = true; - } - } else { - skipCalculation_${fieldId} = true; - } - `; - } else if (filter.__is_special_rub_field) { - return ` - // Фильтр со ссылкой на специальное поле квартала (rüb) или года (il) - if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { - // Получаем значения квартала и года - const docValues = { - rüb: frm.doc["rüb"], - il: frm.doc["il"] - }; - - // Проверяем, что поле rüb имеет допустимое значение - if (!docValues.rüb || typeof docValues.rüb !== 'string') { - skipCalculation_${fieldId} = true; - return; - } - - // Проверяем, что поле il имеет допустимое значение - if (!docValues.il || isNaN(parseInt(docValues.il))) { - skipCalculation_${fieldId} = true; - return; - } - - // Обрабатываем специальные поля rüb и il для получения диапазона дат - const dateRange = processQuarterDateField(docValues); - - if (dateRange) { - // Добавляем два фильтра для диапазона дат - if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === ">=")) - filters_${fieldId}.push(["${filter.field}", ">=", dateRange.start]); - if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === "<")) - filters_${fieldId}.push(["${filter.field}", "<=", dateRange.end]); - } else { - skipCalculation_${fieldId} = true; - } - } else { - skipCalculation_${fieldId} = true; - } - `; - } else { - return ` - // Фильтр со ссылкой на поле документа: ${filter.docField} - if (frm.doc["${filter.docField}"] !== undefined) { - filters_${fieldId}.push(["${filter.field}", "${filter.operator}", frm.doc["${filter.docField}"]]); - } else { - skipCalculation_${fieldId} = true; - } - `; - } - } else { - // Обычные статические фильтры - return ` - // Статический фильтр - filters_${fieldId}.push(["${filter.field}", "${filter.operator}", "${filter.value}"]); - `; - } - }).join('\n ')} - - // Проверяем, нужно ли пропустить расчет - if (skipCalculation_${fieldId}) { - if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; - frm.doc.__universalFields["${fieldId}"] = 0; - return; - } - - const result_${fieldId} = await frappe.db.get_list("${field.doctype}", { - fields: ["${field.fieldname}"], - filters: filters_${fieldId}, - limit: 1000 - }); - - if (result_${fieldId} && result_${fieldId}.length > 0) { - result_${fieldId}.forEach(r => { - univ_${fieldId} += (parseFloat(r.${field.fieldname}) || 0); - }); - } - - // Сохраняем результат в временное поле документа - if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; - frm.doc.__universalFields["${fieldId}"] = univ_${fieldId}; - } catch (error) { - if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; - frm.doc.__universalFields["${fieldId}"] = 0; - }`; - }).filter(code => code !== '').join('\n')} - } - `; - - // Добавляем функцию для обработки специальных полей с кварталом - universalFieldsCode += ` - // Функция для обработки квартала (rüb) и года (il) - function processQuarterDateField(docValues) { - // Проверяем, есть ли у нас значения rüb и il - if (docValues && docValues.rüb && docValues.il) { - // Получаем номер квартала (преобразуем из строки вида "1 Rüb" в число 1) - const quarterStr = docValues.rüb; - const quarterMatch = quarterStr.match(/(\\d+)/); - - if (!quarterMatch) { - return null; - } - - const quarterNum = parseInt(quarterMatch[1]); - - // Получаем год из поля il или используем текущий год - let year = docValues.il || new Date().getFullYear(); - - // Определяем начальный и конечный месяц квартала - let startMonth, endMonth; - - switch(quarterNum) { - case 1: - startMonth = 1; // Январь - endMonth = 3; // Март - break; - case 2: - startMonth = 4; // Апрель - endMonth = 6; // Июнь - break; - case 3: - startMonth = 7; // Июль - endMonth = 9; // Сентябрь - break; - case 4: - startMonth = 10; // Октябрь - endMonth = 12; // Декабрь - break; - default: - return null; - } - - // Определяем первый день квартала (1-е число начального месяца) - const firstDay = new Date(year, startMonth - 1, 1); - - // Последний день квартала (последнее число конечного месяца) - const lastDay = new Date(year, endMonth, 0); - - // Форматируем даты в формате YYYY-MM-DD - const firstDayStr = \`\${year}-\${String(startMonth).padStart(2, '0')}-01\`; - const lastDayStr = \`\${year}-\${String(endMonth).padStart(2, '0')}-\${String(lastDay.getDate()).padStart(2, '0')}\`; - - // Возвращаем объект с диапазоном дат - return { - start: firstDayStr, - end: lastDayStr - }; - } - - // Если это не специальное поле или значение не распознано, возвращаем null - return null; - } - `; - - // Добавляем функцию для обработки специальных полей с азербайджанскими месяцами - universalFieldsCode += ` - // Функция для обработки специальных полей с азербайджанскими месяцами - function processAzerbayjanDateField(docValues) { - // Словарь для сопоставления названий месяцев с их номерами - const monthNames = { - "Yanvar": 1, - "Fevral": 2, - "Mart": 3, - "Aprel": 4, - "May": 5, - "İyun": 6, - "İyul": 7, - "Avqust": 8, - "Sentyabr": 9, - "Oktyabr": 10, - "Noyabr": 11, - "Dekabr": 12 - }; - - // Проверяем наличие квартала (rüb) и года (il) - if (docValues && docValues.rüb && docValues.il) { - // Вызываем специальную функцию для обработки квартала - return processQuarterDateField(docValues); - } - - // Проверяем, есть ли у нас значения ay и il - if (docValues && docValues.ay && monthNames[docValues.ay]) { - // Получаем номер месяца - const monthNum = monthNames[docValues.ay]; - - // Получаем год из поля il или используем текущий год - let year = docValues.il || new Date().getFullYear(); - - // Определяем первый день месяца (1-е число) - const firstDay = new Date(year, monthNum - 1, 1); - - // Последний день месяца (получаем последнее число месяца) - const lastDay = new Date(year, monthNum, 0); - - // Форматируем даты в формате YYYY-MM-DD - const firstDayStr = \`\${year}-\${String(monthNum).padStart(2, '0')}-01\`; - const lastDayStr = \`\${year}-\${String(monthNum).padStart(2, '0')}-\${String(lastDay.getDate()).padStart(2, '0')}\`; - - // Возвращаем объект с диапазоном дат - return { - start: firstDayStr, - end: lastDayStr - }; - } - - // Если это не специальное поле или значение не распознано, возвращаем null - return null; - } - `; - return universalFieldsCode; -} - -function generateRefreshFunction(formula_data) { - // Получаем список полей, для которых созданы формулы - const formulaFields = Object.keys(formula_data); - - // Фильтруем, чтобы брать только поля с непустыми формулами - const validFields = formulaFields.filter(fieldname => - formula_data[fieldname] && - formula_data[fieldname].formula && - formula_data[fieldname].formula.trim() - ); - - let refreshCode = ""; - - // Если ay не является полем с формулой, добавляем обработчики для ay и il - if (!formulaFields.includes('ay')) { - refreshCode += ` "ay": async function(frm) { - // Вычисляем универсальные поля только когда меняется ay - await calculate_universal_fields(frm); - },\n`; - - refreshCode += ` "il": async function(frm) { - // Вычисляем универсальные поля только когда меняется il - await calculate_universal_fields(frm); - },\n`; - } - - return refreshCode; -} - -// Модифицированный вариант функции для безопасного включения JSON в строку кода -function generateTransformExpressionFunction(universal_fields, report_fields) { - return ` - function transform_expression(expression, for_child_table = false, table_name = null) { - let transformed = expression; - let needsAsync = false; - let reportCalls = []; - - // === Обработка report(...) === - const reportRegex = /report\\\\(([^)]+)\\\\)/g; - let match; - - // Находим все вызовы report() в выражении - while ((match = reportRegex.exec(expression)) !== null) { - const reportName = match[1]; // Имя поля отчета - const fullMatch = match[0]; // Полный текст match - "report(xyz)" - needsAsync = true; - - // Создаем уникальный плейсхолдер - const placeholder = \`__REPORT_\${reportCalls.length}__\`; - - // Ищем соответствующее поле отчета по имени - ${Object.keys(report_fields).map(fieldId => { - const field = report_fields[fieldId]; - if (!field || !field.name) return ''; - - // Создаем безопасную строку для JSON-фильтров - let filtersJson = "{}"; - try { - // Парсим JSON фильтров из поля отчета - const filtersObj = JSON.parse(field.filters_json || "{}"); - - // Заменяем значения компании на специальный маркер, если это не маркер - if (filtersObj.company && typeof filtersObj.company === 'string' && - !filtersObj.company.startsWith('$')) { - filtersObj.company = "$DefaultCompany"; - } - - // Сохраняем оригинальные маркеры дат, если они уже есть - // Иначе устанавливаем маркер $AyIlSpecial как значение по умолчанию - Object.keys(filtersObj).forEach(key => { - if (key.toLowerCase().includes('date') || - key.toLowerCase().includes('from') || - key.toLowerCase().includes('to')) { - - const value = filtersObj[key]; - - // Если значение уже является маркером, оставляем его - if (typeof value === 'string' && - (value === "$AyIlSpecial" || value === "$RubIlSpecial" || - value.startsWith("doc."))) { - // Сохраняем оригинальное значение - } - // Если это обычная строка, заменяем на маркер $AyIlSpecial - else if (typeof value === 'string') { - filtersObj[key] = "$AyIlSpecial"; - } - } - }); - - // Конвертируем объект в строку JSON и экранируем спецсимволы - filtersJson = JSON.stringify(filtersObj); - } catch (e) { - filtersJson = "{}"; - } - - return ` - // Проверяем совпадение имени отчета - if (reportName === "${field.name}") { - reportCalls.push({ - placeholder: placeholder, - reportName: "${field.report}", - filters: JSON.parse(${JSON.stringify(filtersJson)}), - fieldName: "${field.field_name}", - aggregation: "${field.aggregation || 'sum'}" - }); - - // Заменяем вызов report() на плейсхолдер - transformed = transformed.replace(fullMatch, placeholder); - }`; - }).join('')} - } - - // === Обработка universal полей === - ${Object.keys(universal_fields).map(fieldId => { - const field = universal_fields[fieldId]; - if (!field || !field.name) return ''; - return `transformed = transformed.replace(/univ\\\\(${field.name}\\\\)/g, "(frm.doc.__universalFields && frm.doc.__universalFields['${fieldId}'] || 0)");`; - }).filter(Boolean).join('\n')} - - // === Обработка val() и row() === - - for (const prefix of tablePrefixes) { - const sumPattern = new RegExp(\`sum\\\\\\\\(\${prefix}_([^)]+)\\\\\\\\)\`, 'g'); - transformed = transformed.replace(sumPattern, (m, f) => - \`(frm.doc["\${prefix}"] && frm.doc["\${prefix}"].reduce((s, r) => s + (parseFloat(r["\${f}"]) || 0), 0) || 0)\` - ); - - const valPattern = new RegExp(\`val\\\\\\\\(\${prefix}_([^)]+)\\\\\\\\)\`, 'g'); - transformed = transformed.replace(valPattern, (m, f) => - \`(frm.doc["\${prefix}"] && frm.doc["\${prefix}"].reduce((s, r) => s + (parseFloat(r["\${f}"]) || 0), 0) || 0)\` - ); - } - - if (for_child_table) { - // ИСПРАВЛЕНО: корректная обработка row() в контексте табличной части - transformed = transformed.replace(/row\\\\(([^)]+)\\\\)/g, (m, f) => - \`(parseFloat(row["\${f}"]) || 0)\` - ); - - transformed = transformed.replace(/val\\\\(([^)]+)\\\\)/g, (m, f) => { - if (f.startsWith(table_name + '_')) { - return \`(parseFloat(row["\${f.slice(table_name.length + 1)}"]) || 0)\`; - } - return \`(parseFloat(frm.doc["\${f}"]) || 0)\`; - }); - } else { - transformed = transformed.replace(/val\\\\(([^)]+)\\\\)/g, (m, f) => - \`(parseFloat(frm.doc["\${f}"]) || 0)\` - ); - - transformed = transformed.replace(/row\\\\(([^)]+)\\\\)/g, () => - \`0 /* ERROR: row() вне табличной части */\` - ); - } - - transformed = transformed.replace(/(frm\\\\.doc\\\\["[^"]+"]|\\\\d+|row\\\\["[^"]+"])\\\\s*([+\\\\-])\\\\s*(\\\\d+(\\\\.\\\\d+)?)%/g, - (match, base, op, percent) => \`\${base} \${op} ((\${base} * \${percent}) / 100)\` - ); - - return { transformed, needsAsync, reportCalls }; - }`; -} - -function processFunctionField(field_info, fieldname, functions, table_child_scripts, - table_doctypes, main_script, target_doctype) { - -let formula = field_info.formula.trim(); - -// Если формула пустая, просто выходим -if (!formula) { -return; -} - -let triggers = field_info.triggers ? field_info.triggers.split(',').map(t => t.trim()) : []; -let is_table_field = field_info.is_table_field; - -let modified_formula = `transform_expression("${formula}", ${is_table_field})`; - -if (is_table_field) { -// Для поля табличной части -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() -}; -} - -// Добавляем функцию расчета для текущего поля -table_child_scripts[table_name].calculations.push({ -field: field_name_in_table, -formula: `transform_expression("${formula}", true, "${table_name}")` -}); - -// Добавляем триггеры -triggers.forEach(trigger => { -table_child_scripts[table_name].fields.add(trigger); -}); -} else { -// Обычная функция расчета для поля основного документа -functions.push(` -function calculate_${fieldname}(frm) { -try { -let formula_expr = ${modified_formula}; -let result = eval(formula_expr); -frm.set_value("${fieldname}", result); -} catch (e) { -} -}`); - -// Добавляем триггеры для основного документа, но только если формула не пустая и есть триггеры -if (triggers.length > 0) { -triggers.forEach(trigger => { -main_script += ` "${trigger}": function(frm) { calculate_${fieldname}(frm); },\n`; -}); -} -} -} - -function generateTableScripts(table_child_scripts, functions) { - let tableScripts = ''; - - Object.keys(table_child_scripts).forEach(table_name => { - let childDoctype = table_child_scripts[table_name].doctype; - let table_script = `\n\nfrappe.ui.form.on("${childDoctype}", {\n`; - - // Создаем общую функцию для расчета формул в строке таблицы - let calcFunction = ` - async function calculate_${tableName}_row(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - - try { - ${usesUniversalFields ? 'await calculate_universal_fields(frm);' : ''} - - const { transformed, needsAsync, reportCalls } = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}"); - - let finalExpression = transformed; - - if (reportCalls && reportCalls.length > 0) { - const reportResults = []; - - for (const call of reportCalls) { - const result = await getReportFieldValue( - frm, - call.reportName, - call.filters, - call.fieldName, - call.aggregation - ); - reportResults.push({ placeholder: call.placeholder, value: result }); - } - - for (const result of reportResults) { - finalExpression = finalExpression.replace(result.placeholder, result.value); - } - } - - let result = 0; - try { - result = eval(finalExpression); - } catch (e) { - } - - // Обновляем значение - row["${calc.field}"] = isNaN(result) ? 0 : result; - frm.refresh_field("${tableName}"); - } catch (error) { - } - }`; - - table_child_scripts[tableName].calculations.forEach(calc => { - calcFunction += ` - // Расчет для поля ${calc.field} - try { - const transformed = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}").transformed; - row["${calc.field}"] = eval(transformed); - } catch (e) { - row["${calc.field}"] = 0; - } - `; - }); - - calcFunction += ` - frm.refresh_field("${tableName}"); - } catch (e) { - } - }`; - - functions.push(calcFunction); - - // Добавляем обработчики для каждого триггерного поля - Array.from(table_child_scripts[table_name].fields).forEach(field => { - table_script += ` "${field}": function(frm, cdt, cdn) { calculate_${table_name}_row(frm, cdt, cdn); },\n`; - }); - - table_script += `});`; - tableScripts += table_script; - }); - - return tableScripts; -} - -function update_or_create_client_script(target_doctype, script_code) { - // Имя для нашего клиентского скрипта формул - const formula_script_name = `Formula_Editor_Script_for_${target_doctype}`; - - frappe.call({ - method: "frappe.client.get_list", - args: { - doctype: "Client Script", - filters: { dt: target_doctype }, - fields: ["name", "script"] - }, - callback: function(r) { - if (r.message && r.message.length > 0) { - // Находим наш скрипт по имени или по содержимому - let formula_script = r.message.find(script => - script.name === formula_script_name || - (script.script && script.script.includes('// Generated by Formula Editor')) - ); - - if (formula_script) { - - frappe.call({ - method: "frappe.client.set_value", - args: { - doctype: "Client Script", - name: formula_script.name, - fieldname: { - script: script_code, - // Обновляем имя, если оно было другим - name: formula_script_name - } - }, - callback: function(response) { - if (response.message) { - } else { - } - } - }); - } else { - createNewFormulaScript(target_doctype, script_code, formula_script_name); - } - } else { - createNewFormulaScript(target_doctype, script_code, formula_script_name); - } - } - }); -} - -function createNewFormulaScript(target_doctype, script_code, script_name) { - // Добавляем комментарий для идентификации скрипта в будущем - const marked_script_code = `// Generated by Formula Editor\n${script_code}`; - - frappe.call({ - method: "frappe.client.insert", - args: { - doc: { - doctype: "Client Script", - name: script_name, - dt: target_doctype, - script_type: "Client", - script: marked_script_code, - enabled: 1 - } - }, - callback: function(response) { - if (response.message) { - } else { - } - } - }); -} - // UI компоненты - создание и обработка полей function generate_dynamic_fields(frm, doctype, callback) { // Получаем числовые поля для формул @@ -1723,7620 +692,4 @@ function generate_dynamic_fields(frm, doctype, callback) { }); } }); -} - -// Создание всех модальных окон -function createModals() { - createFormulaBuilderModal(); - createTriggerFieldsModal(); - createUniversalFieldsModal(); - createReportFieldModal(); - createTableRowFormulaModal(); - createTableRowFieldSelectorModal(); -} - -function handleReportFieldInFormula(fieldName, $resultContainer) { - const reportFields = JSON.parse(cur_frm.doc.report_fields_storage || "{}"); - const field = Object.values(reportFields).find(f => f.name === fieldName); - - if (!field) return null; - - const formulaText = `report(${field.name})`; - const $token = $(`${formulaText} - × - `); - - $resultContainer.append($token).append(' '); - - // Добавляем обработчики для токенов - addTokenHandlers($resultContainer); - - return $token; -} - -// Создание HTML для поля формулы -function createFormulaFieldHtml(field, dataAttr) { - return $('
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '' + - '
' + - '' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '' + - '
' + - '' + - '
' + - '
' + - '
' + - '
'); -} - -// Общие функции и утилиты -function determineFieldType(fieldName, group, isInTableField) { - // Проверяем, является ли поле ссылкой на строку табличной части - const rowPattern = /^([a-z_]+)_row_(\d+)_([a-z_]+)$/i; - const rowMatch = fieldName.match(rowPattern); - - if (rowMatch) { - const tableName = rowMatch[1]; - const rowIndex = rowMatch[2]; - const fieldNameInRow = rowMatch[3]; - - return { - type: 'table_row', - tableName: tableName, - rowIndex: rowIndex, - fieldName: fieldNameInRow, - fullFieldName: fieldName - }; - } - - // Получаем части имени поля - // Проверяем существование имен групп "items", "taxes" и т.д. без подчеркивания - const tablePrefixes = ["items", "taxes", "packed_items", "payments", "advances", "timesheets", "payment_schedule", "sales_team"]; - - // Проверяем, начинается ли fieldName с известного префикса таблицы - let isTableField = false; - let tableName = ""; - - for (const prefix of tablePrefixes) { - if (fieldName.startsWith(prefix)) { - // Это поле табличной части без подчеркивания (например, "itemsqty") - isTableField = true; - tableName = prefix; - break; - } - } - - // Если не нашли по префиксу, проверяем по первой части с подчеркиванием - if (!isTableField) { - const parts = fieldName.split('_'); - if (parts.length > 1) { - const potentialTableName = parts[0]; - if (tablePrefixes.includes(potentialTableName)) { - isTableField = true; - tableName = potentialTableName; - } - } - } - - // Определяем тип формулы - if (isTableField) { - // Для полей таблицы - if (isInTableField) { - // В табличной части используем row() - // Извлекаем имя поля без префикса таблицы - let fieldNameWithoutTable = fieldName; - if (fieldName.startsWith(tableName)) { - fieldNameWithoutTable = fieldName.substring(tableName.length); - // Если есть подчеркивание, удаляем его - if (fieldNameWithoutTable.startsWith('_')) { - fieldNameWithoutTable = fieldNameWithoutTable.substring(1); - } - } - - return { - type: 'row', - fieldName: fieldNameWithoutTable - }; - } else { - // В основном документе используем sum() - return { - type: 'sum', - fieldName: fieldName, - tableName: tableName - }; - } - } else { - // Для обычных полей всегда val() - return { - type: 'val', - fieldName: fieldName - }; - } -} - -function setEndOfContentEditable(contentEditableElement) { - let range, selection; - range = document.createRange(); - range.selectNodeContents(contentEditableElement); - range.collapse(false); - selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); -} - -// Функции для работы с формулами -function getFormulaText($container) { - // Собираем все токены и текст в строку - let formula = ''; - - $container.contents().each(function() { - if (this.nodeType === Node.TEXT_NODE) { - formula += this.textContent; - } else { - const $elem = $(this); - if ($elem.hasClass('formula-token')) { - formula += $elem.data('value'); - } - } - }); - - // Удаляем лишние пробелы - return formula.replace(/\s+/g, ' ').trim(); -} - -function parseAndRenderFormula($container, formula, isTableField) { - if (!formula) return; - - // Регулярные выражения для поиска функций и операторов - const funcRegex = /(val|row|sum|univ|report)\(([^)]+)\)/g; - const operatorsRegex = /([+\-*/()%])/g; - - // Сохраняем текущую позицию - let lastIndex = 0; - let match; - let formulaParts = []; - - // Поиск всех функций - while ((match = funcRegex.exec(formula)) !== null) { - // Добавляем текст до функции - if (match.index > lastIndex) { - formulaParts.push({ - type: 'text', - value: formula.substring(lastIndex, match.index) - }); - } - - // Добавляем функцию - formulaParts.push({ - type: 'function', - value: match[0], - func: match[1], - param: match[2] - }); - - lastIndex = match.index + match[0].length; - } - - // Добавляем оставшийся текст после последней функции - if (lastIndex < formula.length) { - formulaParts.push({ - type: 'text', - value: formula.substring(lastIndex) - }); - } - - // Разбираем операторы в частях, которые не являются функциями - let finalParts = []; - formulaParts.forEach(part => { - if (part.type === 'function') { - finalParts.push(part); - } else if (part.type === 'text') { - let text = part.value; - lastIndex = 0; - - // Сбрасываем lastIndex для operatorsRegex - operatorsRegex.lastIndex = 0; - - // Разбираем операторы - while ((match = operatorsRegex.exec(text)) !== null) { - // Добавляем текст до оператора - if (match.index > lastIndex) { - const textBefore = text.substring(lastIndex, match.index).trim(); - if (textBefore) { - finalParts.push({ - type: 'text', - value: textBefore - }); - } - } - - // Добавляем оператор - finalParts.push({ - type: 'operator', - value: match[1] - }); - - lastIndex = match.index + match[1].length; - } - - // Добавляем оставшийся текст после последнего оператора - if (lastIndex < text.length) { - const textAfter = text.substring(lastIndex).trim(); - if (textAfter) { - finalParts.push({ - type: 'text', - value: textAfter - }); - } - } - } - }); - - // Рендерим части формулы - finalParts.forEach(part => { - if (part.type === 'function') { - // Определяем класс в зависимости от типа функции - let tokenClass = 'field'; - if (part.func === 'univ') { - tokenClass = 'field universal-field'; - } else if (part.func === 'report') { - tokenClass = 'field report-field'; - } - - const $token = $(`${part.value}×`); - $container.append($token).append(' '); - } else if (part.type === 'operator') { - const $token = $(`${part.value}×`); - $container.append($token).append(' '); - } else if (part.type === 'text') { - $container.append(document.createTextNode(`${part.value} `)); - } - }); - - // Добавляем обработчики для токенов - addTokenHandlers($container); - - return formula; -} - -function addTokenHandlers($container) { - // Удаление токенов - $container.find('.token-remove').off('click').on('click', function(e) { - e.stopPropagation(); - $(this).parent().remove(); - }); - - // Перетаскивание токенов - $container.find('.formula-token').each(function() { - $(this).attr('draggable', 'true'); - - $(this).off('dragstart').on('dragstart', function(e) { - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'token', - value: $(this).data('value'), - html: $(this)[0].outerHTML - })); - window.draggedElement = this; - }); - - $(this).off('dragend').on('dragend', function() { - window.draggedElement = null; - }); - }); -} - -// Функции для создания модальных окон -function createFormulaBuilderModal() { -if ($("#formula_builder_modal").length) { - return; -} - -const modalHtml = ` - -`; - -// Стили с поддержкой перетаскивания -const modalStyles = ` - -`; - -$('body').append(modalHtml); -$('head').append(modalStyles); -} - -function createTriggerFieldsModal() { - if ($("#trigger_fields_modal").length) { - return; - } - - const modalHtml = ` - - `; - - $('body').append(modalHtml); -} - -function createUniversalFieldsModal() { - if ($("#universal_fields_modal").length) { - return; - } - - const modalHtml = ` - `; - - $('body').append(modalHtml); -} - -// Обработчики событий редактора формул -function attachFormulaBuilderEvents(frm, fields, grouped_fields) { - // Сделаем поля ввода формул редактируемыми - $(frm.wrapper).find('.formula-input').prop('readonly', false); - - // Сначала удаляем существующие обработчики событий - $(frm.wrapper).off('click', '.open-formula-builder'); - - // Обработчик для кнопок открытия визуального редактора - $(frm.wrapper).on('click', '.open-formula-builder', function() { - const fieldname = $(this).data('fieldname'); - const isTableField = $(this).data('is-table') === true; - const currentFormula = $(frm.wrapper).find(`.formula-input[data-fieldname="${fieldname}"]`).val(); - - openFormulaBuilder(frm, fieldname, isTableField, currentFormula, fields, grouped_fields); - return false; - }); -} - -function openFormulaBuilder(frm, targetFieldname, isTableField, currentFormula, fields, grouped_fields) { - const $modal = $("#formula_builder_modal"); - const $resultContainer = $modal.find(".formula-result"); - - // Очистка предыдущих обработчиков и данных - $modal.off('click', '.field-btn'); - $modal.off('click', '.universal-field-btn'); - $modal.off('click', '.operator-btn'); - $modal.find('.apply-formula').off('click'); - $resultContainer.empty(); - $modal.find(".field-groups-container").empty(); - - // Заполняем текущую формулу, если она есть - if (currentFormula) { - parseAndRenderFormula($resultContainer, currentFormula, isTableField); - } - - // Заполняем панель с полями - populateFieldsPanel($modal, frm, grouped_fields, isTableField); - - // Сохраняем данные о текущем поле - $modal.data("target-field", targetFieldname); - $modal.data("is-table-field", isTableField); - - // Обработчики для кнопок операторов - $modal.find('.operator-btn').off('click').on('click', function() { - const operator = $(this).data('operator'); - addTokenToFormula(operator, 'operator', $resultContainer); - }); - - // Обработчик для кнопки применения формулы - $modal.find('.apply-formula').off('click').on('click', function() { - const formula = getFormulaText($resultContainer); - const $targetInput = $(frm.wrapper).find(`.formula-input[data-fieldname="${targetFieldname}"]`); - $targetInput.val(formula); - $modal.modal('hide'); - }); - - // Обработчики для кнопок полей - используем делегирование событий - $modal.off('click', '.field-btn').on('click', '.field-btn', function() { - const fieldname = $(this).data('fieldname'); - addTokenToFormula(fieldname, 'field', $resultContainer, isTableField, $(this).data('group')); - }); - - // Обработчики для кнопок универсальных полей - $modal.off('click', '.universal-field-btn').on('click', '.universal-field-btn', function() { - const fieldId = $(this).data('field-id'); - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - const field = universalFields[fieldId]; - - if (field) { - const formulaText = `univ(${field.name})`; - const $token = $(`${formulaText}×`); - $resultContainer.append($token).append(' '); - - // Добавляем обработчики для токенов - addTokenHandlers($resultContainer); - - // Переносим курсор в конец контейнера - setEndOfContentEditable($resultContainer[0]); - } - }); - - // Показываем модальное окно - $modal.modal('show'); - - // Инициализируем перетаскивание - initDragAndDrop($modal, $resultContainer, isTableField, frm); - - // Настраиваем редактируемый контейнер - setupEditableContainer($resultContainer); - - // Фокусируемся на редактируемом поле - $resultContainer.focus(); -} - -function addTokenToFormula(token, type, $resultContainer, isTableField = false, group = null, frm = null) { - if (type === 'field') { - let formulaText; - - // Определяем тип поля и формулу - const fieldInfo = determineFieldType(token, group, isTableField); - - if (fieldInfo.type === 'table_row') { - // Для полей конкретных строк таблиц - formulaText = `val(${fieldInfo.fullFieldName})`; - } else if (fieldInfo.type === 'row') { - formulaText = `row(${fieldInfo.fieldName})`; - } else if (fieldInfo.type === 'sum') { - let tableName = fieldInfo.tableName || token.match(/^([a-z_]+)/)[1]; - let fieldWithoutPrefix = token.substring(tableName.length); - if (fieldWithoutPrefix.startsWith('_')) { - fieldWithoutPrefix = fieldWithoutPrefix.substring(1); - } - - formulaText = `sum(${tableName}_${fieldWithoutPrefix})`; - } else { - formulaText = `val(${fieldInfo.fieldName})`; - } - - const $token = $(`${formulaText}×`); - $resultContainer.append($token).append(' '); - } else if (type === 'operator') { - const $token = $(`${token}×`); - $resultContainer.append($token).append(' '); - } else if (type === 'universal-field') { - // Обработка для универсальных полей - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - const field = universalFields[token]; - if (field) { - const formulaText = `univ(${field.name})`; - const $token = $(`${formulaText}×`); - $resultContainer.append($token).append(' '); - } - } else if (type === 'report-field') { - // Обработка для полей отчетов - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - const field = reportFields[token]; - if (field) { - const formulaText = `report(${field.name})`; - const $token = $(`${formulaText}×`); - $resultContainer.append($token).append(' '); - } - } - - // Переносим курсор в конец контейнера - setEndOfContentEditable($resultContainer[0]); - - // Добавляем обработчики для токенов - addTokenHandlers($resultContainer); -} - -function populateFieldsPanel($modal, frm, grouped_fields, isTableField) { - const $container = $modal.find(".field-groups-container"); - $container.empty(); - - // Добавляем секцию предопределенных событий - const $predefinedEventsGroup = $(` -
-
Предопределенные события
-
-
- `); - - const $predefinedButtonsContainer = $predefinedEventsGroup.find('.field-buttons-container'); - - // Добавляем кнопки для предопределенных событий - const predefinedEvents = [ - { name: "При открытии документа", id: "При открытии документа" } - ]; - - predefinedEvents.forEach(event => { - const $eventBtn = $(` - - `); - - $predefinedButtonsContainer.append($eventBtn); - }); - - // Добавляем группу предопределенных событий в начало контейнера - $container.prepend($predefinedEventsGroup); - - // Добавляем стандартные поля - Object.keys(grouped_fields).forEach(group => { - const $fieldGroup = $(` -
-
${group}
-
-
- `); - - const $buttonsContainer = $fieldGroup.find('.field-buttons-container'); - - grouped_fields[group].forEach(field => { - // Определяем, является ли это поле таблицы или нет - const isTableGroupField = group !== "Основные поля"; - - // Создаем кнопку поля с правильными атрибутами - const btnClass = isTableGroupField ? "field-btn table-field-btn" : "field-btn"; - const $fieldBtn = $(` - - `); - - $buttonsContainer.append($fieldBtn); - }); - - $container.append($fieldGroup); - }); - - // Добавляем секцию универсальных полей - if (frm.doc.universal_fields_storage) { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - - if (Object.keys(universalFields).length > 0) { - const $universalFieldGroup = $(` -
-
Универсальные поля
-
-
- `); - - const $universalButtonsContainer = $universalFieldGroup.find('.field-buttons-container'); - - Object.keys(universalFields).forEach(fieldId => { - const field = universalFields[fieldId]; - - if (!field || !field.name) return; - - const $fieldBtn = $(` - - `); - - $universalButtonsContainer.append($fieldBtn); - }); - - $container.append($universalFieldGroup); - } - } - - if (frm.doc.report_fields_storage) { - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - - if (Object.keys(reportFields).length > 0) { - const $reportFieldGroup = $(` -
-
Поля отчетов
-
-
- `); - - const $reportButtonsContainer = $reportFieldGroup.find('.field-buttons-container'); - - Object.keys(reportFields).forEach(fieldId => { - const field = reportFields[fieldId]; - - if (!field || !field.name) return; - - const $fieldBtn = $(` - - `); - - $reportButtonsContainer.append($fieldBtn); - }); - - $container.append($reportFieldGroup); - } - } - - // Добавляем группу для полей строк табличных частей - const $tableRowGroup = $(` -
-
Поля строк табличных частей
-
- -
-
- `); - - $container.append($tableRowGroup); - - // Добавляем обработчик для кнопки выбора поля строки табличной части - $tableRowGroup.find('.select-table-row-field').on('click', function() { - // Определяем целевой контейнер в зависимости от контекста - const targetContainer = $modal.hasClass('trigger-focus') ? - $modal.find(".trigger-result") : - $modal.find(".formula-result"); - - openTableRowFieldSelector(frm, targetContainer, $modal.hasClass('trigger-focus')); - }); -} - -function initDragAndDrop($modal, $resultContainer, isTableField, frm) { - // Очищаем предыдущие обработчики - $modal.find('.field-btn, .operator-btn, .universal-field-btn').off('dragstart'); - $resultContainer.off('dragover dragleave drop'); - - // Настраиваем перетаскивание существующих токенов - setupTokenDragging($resultContainer); - - // Настраиваем перетаскивание полей из панели - $modal.find('.field-btn').off('dragstart').on('dragstart', function(e) { - const fieldname = $(this).data('fieldname'); - const label = $(this).data('label'); - const group = $(this).data('group'); - - // Проверяем принадлежность к группе - const isTableFieldGroup = group !== "Основные поля"; - const fieldData = { - type: 'field', - fieldname: fieldname, - label: label, - group: group, - isTableField: isTableFieldGroup - }; - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(fieldData)); - }); - - // Настраиваем перетаскивание универсальных полей - $modal.find('.universal-field-btn').off('dragstart').on('dragstart', function(e) { - const fieldId = $(this).data('field-id'); - const fieldname = $(this).data('fieldname'); - const label = $(this).data('label'); - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'universal-field', - fieldId: fieldId, - fieldname: fieldname, - label: label - })); - }); - - // Настраиваем перетаскивание операторов - $modal.find('.operator-btn').off('dragstart').on('dragstart', function(e) { - const operator = $(this).data('operator'); - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'operator', - value: operator - })); - }); - - // Обработчики для целевого контейнера - $resultContainer.off('dragover').on('dragover', function(e) { - e.preventDefault(); - e.stopPropagation(); - $(this).addClass('drag-hover'); - - updateDropMarker(e, $(this)); - }); - - $resultContainer.off('dragleave').on('dragleave', function(e) { - $(this).removeClass('drag-hover'); - $(this).find('.drop-marker').remove(); - }); - - $resultContainer.off('drop').on('drop', function(e) { - e.preventDefault(); - e.stopPropagation(); - $(this).removeClass('drag-hover'); - $(this).find('.drop-marker').remove(); - - handleDrop(e, $(this), isTableField, frm); - }); -} - -function setupTokenDragging($container) { - $container.find('.formula-token').each(function() { - $(this).attr('draggable', 'true'); - - $(this).off('dragstart').on('dragstart', function(e) { - // Сохраняем ссылку на текущий элемент - window.draggedElement = this; - - // Добавляем данные для переноса - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'token', - value: $(this).data('value'), - html: $(this)[0].outerHTML - })); - - // Добавляем класс перетаскивания - $(this).addClass('dragging'); - - return true; - }); - - // При завершении перетаскивания - $(this).off('dragend').on('dragend', function(e) { - $(this).removeClass('dragging'); - window.draggedElement = null; - }); - }); -} - -function updateDropMarker(e, $container) { - const x = e.originalEvent.clientX; - const y = e.originalEvent.clientY; - - let position = { element: null, position: 'after' }; - - // Проходим по всем дочерним элементам контейнера - $container.contents().each(function() { - if (this.nodeType === Node.TEXT_NODE) return; - - const rect = this.getBoundingClientRect(); - - // Если курсор находится над элементом - if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { - position.element = this; - - // Определяем, до или после элемента - if (x < rect.left + rect.width / 2) { - position.position = 'before'; - } else { - position.position = 'after'; - } - - return false; // Прерываем перебор - } - }); - - $container.find('.drop-marker').remove(); - - if (position.element) { - const $marker = $(''); - - if (position.position === 'before') { - $(position.element).before($marker); - } else { - $(position.element).after($marker); - } - } -} - -// Исправляем функцию handleDrop для корректной обработки полей отчетов -function handleDrop(e, $container, isTableField, frm) { - try { - // Получаем данные из события перетаскивания - const dataTransfer = e.originalEvent.dataTransfer.getData('text/plain'); - - // Проверяем, что данные не пустые - if (!dataTransfer) { - return; - } - - const data = JSON.parse(dataTransfer); - - const x = e.originalEvent.clientX; - const y = e.originalEvent.clientY; - - let position = findDropPosition(x, y, $container); - - // Обработка перетаскивания существующих токенов - if (data.type === 'token' && window.draggedElement) { - handleExistingTokenDrop(window.draggedElement, position, $container); - return; - } - - // Создание нового элемента - let $newElement; - - if (data.type === 'universal-field') { - // Получаем универсальные поля - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - const field = universalFields[data.fieldId]; - - if (field) { - const formulaText = `univ(${field.name})`; - $newElement = $(`${formulaText}×`); - } - } else if (data.type === 'report-field') { - // Обработка поля отчета - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - const field = reportFields[data.fieldId]; - - if (field) { - const formulaText = `report(${field.name})`; - $newElement = $(`${formulaText}×`); - } - } else if (data.type === 'field') { - $newElement = createFieldToken(data, isTableField); - } else if (data.type === 'operator') { - $newElement = createOperatorToken(data.value); - } - - // Вставляем новый элемент - if ($newElement) { - insertElementAtPosition($newElement, position, $container); - - // Инициализируем события для нового элемента - initializeTokenEvents($newElement); - } - } catch (error) { - } -} - -// Добавляем поддержку полей отчетов в populateFieldsPanel -function populateFieldsPanel($modal, frm, grouped_fields, isTableField) { - const $container = $modal.find(".field-groups-container"); - $container.empty(); - - // Добавляем секцию предопределенных событий - const $predefinedEventsGroup = $(` -
-
Предопределенные события
-
-
- `); - - const $predefinedButtonsContainer = $predefinedEventsGroup.find('.field-buttons-container'); - - // Добавляем кнопки для предопределенных событий - const predefinedEvents = [ - { name: "При открытии документа", id: "При открытии документа" } - ]; - - predefinedEvents.forEach(event => { - const $eventBtn = $(` - - `); - - $predefinedButtonsContainer.append($eventBtn); - }); - - // Добавляем группу предопределенных событий в начало контейнера - $container.prepend($predefinedEventsGroup); - - // Добавляем стандартные поля - Object.keys(grouped_fields).forEach(group => { - const $fieldGroup = $(` -
-
${group}
-
-
- `); - - const $buttonsContainer = $fieldGroup.find('.field-buttons-container'); - - grouped_fields[group].forEach(field => { - // Определяем, является ли это поле таблицы или нет - const isTableGroupField = group !== "Основные поля"; - - // Создаем кнопку поля с правильными атрибутами - const btnClass = isTableGroupField ? "field-btn table-field-btn" : "field-btn"; - const $fieldBtn = $(` - - `); - - $buttonsContainer.append($fieldBtn); - }); - - $container.append($fieldGroup); - }); - - // Добавляем секцию универсальных полей - if (frm.doc.universal_fields_storage) { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - - if (Object.keys(universalFields).length > 0) { - const $universalFieldGroup = $(` -
-
Универсальные поля
-
-
- `); - - const $universalButtonsContainer = $universalFieldGroup.find('.field-buttons-container'); - - Object.keys(universalFields).forEach(fieldId => { - const field = universalFields[fieldId]; - - if (!field || !field.name) return; - - const $fieldBtn = $(` - - `); - - $universalButtonsContainer.append($fieldBtn); - }); - - $container.append($universalFieldGroup); - } - } - - // Добавляем секцию полей отчетов - if (frm.doc.report_fields_storage) { - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - - if (Object.keys(reportFields).length > 0) { - const $reportFieldGroup = $(` -
-
Поля отчетов
-
-
- `); - - const $reportButtonsContainer = $reportFieldGroup.find('.field-buttons-container'); - - Object.keys(reportFields).forEach(fieldId => { - const field = reportFields[fieldId]; - - if (!field || !field.name) return; - - const $fieldBtn = $(` - - `); - - $reportButtonsContainer.append($fieldBtn); - }); - - $container.append($reportFieldGroup); - } - } - - // Добавляем группу для полей строк табличных частей - const $tableRowGroup = $(` -
-
Поля строк табличных частей
-
- -
-
- `); - - $container.append($tableRowGroup); - - // Добавляем обработчик для кнопки выбора поля строки табличной части - $tableRowGroup.find('.select-table-row-field').on('click', function() { - // Определяем целевой контейнер в зависимости от контекста - const targetContainer = $modal.hasClass('trigger-focus') ? - $modal.find(".trigger-result") : - $modal.find(".formula-result"); - - openTableRowFieldSelector(frm, targetContainer, $modal.hasClass('trigger-focus')); - }); -} - -// Обновляем функцию initDragAndDrop для поддержки полей отчетов -function initDragAndDrop($modal, $resultContainer, isTableField, frm) { - // Очищаем предыдущие обработчики - $modal.find('.field-btn, .operator-btn, .universal-field-btn, .report-field-btn').off('dragstart'); - $resultContainer.off('dragover dragleave drop'); - - // Настраиваем перетаскивание существующих токенов - setupTokenDragging($resultContainer); - - // Настраиваем перетаскивание полей из панели - $modal.find('.field-btn').off('dragstart').on('dragstart', function(e) { - const fieldname = $(this).data('fieldname'); - const label = $(this).data('label'); - const group = $(this).data('group'); - - // Проверяем принадлежность к группе - const isTableFieldGroup = group !== "Основные поля"; - const fieldData = { - type: 'field', - fieldname: fieldname, - label: label, - group: group, - isTableField: isTableFieldGroup - }; - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(fieldData)); - }); - - // Настраиваем перетаскивание универсальных полей - $modal.find('.universal-field-btn').off('dragstart').on('dragstart', function(e) { - const fieldId = $(this).data('field-id'); - const fieldname = $(this).data('fieldname'); - const label = $(this).data('label'); - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'universal-field', - fieldId: fieldId, - fieldname: fieldname, - label: label - })); - }); - - // Настраиваем перетаскивание полей отчетов - $modal.find('.report-field-btn').off('dragstart').on('dragstart', function(e) { - const fieldId = $(this).data('field-id'); - const fieldname = $(this).data('fieldname'); - const label = $(this).data('label'); - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'report-field', - fieldId: fieldId, - fieldname: fieldname, - label: label - })); - }); - - // Настраиваем перетаскивание операторов - $modal.find('.operator-btn').off('dragstart').on('dragstart', function(e) { - const operator = $(this).data('operator'); - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'operator', - value: operator - })); - }); - - // Обработчики для целевого контейнера - $resultContainer.off('dragover').on('dragover', function(e) { - e.preventDefault(); - e.stopPropagation(); - $(this).addClass('drag-hover'); - - updateDropMarker(e, $(this)); - }); - - $resultContainer.off('dragleave').on('dragleave', function(e) { - $(this).removeClass('drag-hover'); - $(this).find('.drop-marker').remove(); - }); - - $resultContainer.off('drop').on('drop', function(e) { - e.preventDefault(); - e.stopPropagation(); - $(this).removeClass('drag-hover'); - $(this).find('.drop-marker').remove(); - - handleDrop(e, $(this), isTableField, frm); - }); -} - -// Обновляем функцию для обработки клика по кнопке поля отчета -function attachFormulaBuilderEvents(frm, fields, grouped_fields) { - // Сделаем поля ввода формул редактируемыми - $(frm.wrapper).find('.formula-input').prop('readonly', false); - - // Сначала удаляем существующие обработчики событий - $(frm.wrapper).off('click', '.open-formula-builder'); - - // Обработчик для кнопок открытия визуального редактора - $(frm.wrapper).on('click', '.open-formula-builder', function() { - const fieldname = $(this).data('fieldname'); - const isTableField = $(this).data('is-table') === true; - const currentFormula = $(frm.wrapper).find(`.formula-input[data-fieldname="${fieldname}"]`).val(); - - openFormulaBuilder(frm, fieldname, isTableField, currentFormula, fields, grouped_fields); - return false; - }); -} - -// Добавляем обработчики для кнопок полей отчетов в редакторе формул -function openFormulaBuilder(frm, targetFieldname, isTableField, currentFormula, fields, grouped_fields) { - const $modal = $("#formula_builder_modal"); - const $resultContainer = $modal.find(".formula-result"); - - // Очистка предыдущих обработчиков и данных - $modal.off('click', '.field-btn'); - $modal.off('click', '.universal-field-btn'); - $modal.off('click', '.report-field-btn'); - $modal.off('click', '.operator-btn'); - $modal.find('.apply-formula').off('click'); - $resultContainer.empty(); - $modal.find(".field-groups-container").empty(); - - // Заполняем текущую формулу, если она есть - if (currentFormula) { - parseAndRenderFormula($resultContainer, currentFormula, isTableField); - } - - // Заполняем панель с полями - populateFieldsPanel($modal, frm, grouped_fields, isTableField); - - // Сохраняем данные о текущем поле - $modal.data("target-field", targetFieldname); - $modal.data("is-table-field", isTableField); - - // Обработчики для кнопок операторов - $modal.find('.operator-btn').off('click').on('click', function() { - const operator = $(this).data('operator'); - addTokenToFormula(operator, 'operator', $resultContainer); - }); - - // Обработчик для кнопки применения формулы - $modal.find('.apply-formula').off('click').on('click', function() { - const formula = getFormulaText($resultContainer); - const $targetInput = $(frm.wrapper).find(`.formula-input[data-fieldname="${targetFieldname}"]`); - $targetInput.val(formula); - $modal.modal('hide'); - }); - - // Обработчики для кнопок полей - используем делегирование событий - $modal.off('click', '.field-btn').on('click', '.field-btn', function() { - const fieldname = $(this).data('fieldname'); - addTokenToFormula(fieldname, 'field', $resultContainer, isTableField, $(this).data('group')); - }); - - // Обработчики для кнопок универсальных полей - $modal.off('click', '.universal-field-btn').on('click', '.universal-field-btn', function() { - const fieldId = $(this).data('field-id'); - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - const field = universalFields[fieldId]; - - if (field) { - const formulaText = `univ(${field.name})`; - const $token = $(`${formulaText}×`); - $resultContainer.append($token).append(' '); - - // Добавляем обработчики для токенов - addTokenHandlers($resultContainer); - - // Переносим курсор в конец контейнера - setEndOfContentEditable($resultContainer[0]); - } - }); - - // Обработчики для кнопок полей отчетов - $modal.off('click', '.report-field-btn').on('click', '.report-field-btn', function() { - const fieldId = $(this).data('field-id'); - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - const field = reportFields[fieldId]; - - if (field) { - const formulaText = `report(${field.name})`; - const $token = $(`${formulaText}×`); - $resultContainer.append($token).append(' '); - - // Добавляем обработчики для токенов - addTokenHandlers($resultContainer); - - // Переносим курсор в конец контейнера - setEndOfContentEditable($resultContainer[0]); - } - }); - - // Показываем модальное окно - $modal.modal('show'); - - // Инициализируем перетаскивание - initDragAndDrop($modal, $resultContainer, isTableField, frm); - - // Настраиваем редактируемый контейнер - setupEditableContainer($resultContainer); - - // Фокусируемся на редактируемом поле - $resultContainer.focus(); -} - -function findDropPosition(x, y, $container) { - let position = { element: null, position: 'after' }; - - // Проходим по всем дочерним элементам контейнера - $container.contents().each(function() { - if (this.nodeType === Node.TEXT_NODE) return; - - const rect = this.getBoundingClientRect(); - - // Если курсор находится над элементом - if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { - position.element = this; - - // Определяем, до или после элемента - if (x < rect.left + rect.width / 2) { - position.position = 'before'; - } else { - position.position = 'after'; - } - - return false; // Прерываем перебор - } - }); - - return position; -} - -function handleExistingTokenDrop(draggedElement, position, $container) { - const $dragged = $(draggedElement); - - if (position.element) { - if (position.position === 'before') { - $(position.element).before($dragged); - $(position.element).before(' '); - } else { - $(position.element).after($dragged); - $(position.element).after(' '); - } - } else { - $container.append($dragged); - $container.append(' '); - } - - window.draggedElement = null; -} - -function createUniversalFieldToken(data, frm) { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - const field = universalFields[data.fieldId]; - if (!field) return null; - - const formulaText = `univ(${field.name})`; - return $(`${formulaText}×`); -} - -function createFieldToken(data, isTableField) { - let formulaText; - - // Определяем тип поля и формулу - const fieldInfo = determineFieldType(data.fieldname, data.group, isTableField); - - if (fieldInfo.type === 'row') { - formulaText = `row(${fieldInfo.fieldName})`; - } else if (fieldInfo.type === 'sum') { - formulaText = `sum(${fieldInfo.fieldName})`; - } else { - formulaText = `val(${fieldInfo.fieldName})`; - } - - return $(`${formulaText}×`); -} - -function createOperatorToken(value) { - return $(`${value}×`); -} - -function insertElementAtPosition($element, position, $container) { - if (position.element) { - if (position.position === 'before') { - $(position.element).before($element); - $(position.element).before(' '); - } else { - $(position.element).after($element); - $(position.element).after(' '); - } - } else { - $container.append($element); - $container.append(' '); - } -} - -function initializeTokenEvents($token) { - $token.find('.token-remove').on('click', function(e) { - e.stopPropagation(); - $(this).parent().remove(); - }); - - $token.attr('draggable', 'true'); - - $token.off('dragstart').on('dragstart', function(e) { - window.draggedElement = this; - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'token', - value: $(this).data('value'), - html: $(this)[0].outerHTML - })); - - $(this).addClass('dragging'); - return true; - }); - - $token.off('dragend').on('dragend', function(e) { - $(this).removeClass('dragging'); - window.draggedElement = null; - }); -} - -function setupEditableContainer($container) { - // Обработчик нажатия клавиш в контейнере формулы - $container.off('keydown').on('keydown', function(e) { - // Если нажата клавиша Delete или Backspace - if (e.keyCode === 8 || e.keyCode === 46) { - const selection = window.getSelection(); - - if (selection.rangeCount > 0) { - const range = selection.getRangeAt(0); - - if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) { - // Текстовый узел - обычное удаление - return true; - } else { - // Возможно, выделен токен - const $selectedNode = $(range.commonAncestorContainer); - - if ($selectedNode.hasClass('formula-token') || $selectedNode.parents('.formula-token').length) { - // Удаляем токен - const $token = $selectedNode.hasClass('formula-token') ? - $selectedNode : $selectedNode.parents('.formula-token'); - $token.remove(); - e.preventDefault(); - return false; - } - } - } - } - - return true; - }); - - // Обработчик вставки текста - $container.off('paste').on('paste', function(e) { - e.preventDefault(); - - // Получаем вставляемый текст - let text = ''; - if (e.clipboardData || e.originalEvent.clipboardData) { - text = (e.clipboardData || e.originalEvent.clipboardData).getData('text/plain'); - } else if (window.clipboardData) { - text = window.clipboardData.getData('Text'); - } - - // Вставляем текст как обычный текст - if (text) { - document.execCommand('insertText', false, text); - } - }); -} - -// Функции для работы с полями изменения (триггерами) -function attachTriggerEditorEvents(frm, fields, grouped_fields) { - // Сначала удаляем существующие обработчики событий - $(frm.wrapper).off('click', '.open-trigger-editor'); - - // Обработчик для кнопок открытия редактора полей изменения - $(frm.wrapper).on('click', '.open-trigger-editor', function() { - const fieldname = $(this).data('fieldname'); - const currentTriggers = $(frm.wrapper).find(`.trigger-input[data-fieldname="${fieldname}_triggers"]`).val(); - - openTriggerFieldsEditor(frm, fieldname, currentTriggers, fields, grouped_fields); - return false; - }); -} - -function openTriggerFieldsEditor(frm, targetFieldname, currentTriggers, fields, grouped_fields) { - const $modal = $("#trigger_fields_modal"); - const $resultContainer = $modal.find(".trigger-result"); - - // Очистка предыдущих обработчиков и данных - $modal.off('click', '.trigger-field-btn'); - $modal.off('click', '.select-table-row-trigger-field'); - $modal.find('.apply-triggers').off('click'); - $resultContainer.empty(); - $modal.find(".trigger-field-groups-container").empty(); - - // Заполняем текущие триггеры, если они есть - if (currentTriggers) { - parseAndRenderTriggers($resultContainer, currentTriggers); - } - - // Заполняем панель с полями - populateTriggerFieldsPanel($modal, frm, grouped_fields); - - // Сохраняем данные о текущем поле и сбрасываем флаг table-row - $modal.data("target-field", targetFieldname); - $modal.data("is-for-table-row", false); - - // Обработчик для кнопки применения триггеров - $modal.find('.apply-triggers').off('click').on('click', function() { - const triggers = getTriggerFieldsText($resultContainer); - - // Проверяем, для контейнера строки таблицы или обычного поля - if ($modal.data("is-for-table-row")) { - const $tableRowContainer = $modal.data("table-row-container"); - - // Очищаем контейнер строки таблицы - $tableRowContainer.empty(); - - // Переносим триггеры из модального окна в контейнер строки таблицы - if (triggers) { - parseAndRenderTriggers($tableRowContainer, triggers); - } - } else { - // Стандартное поведение для обычного поля - const $targetInput = $(frm.wrapper).find(`.trigger-input[data-fieldname="${targetFieldname}_triggers"]`); - $targetInput.val(triggers); - } - - $modal.modal('hide'); - }); - - // Обработчики для кнопок полей - $modal.off('click', '.trigger-field-btn').on('click', '.trigger-field-btn', function() { - const fieldname = $(this).data('fieldname'); - - // Определяем тип поля по классу кнопки - let fieldType = 'field'; - if ($(this).hasClass('highlight-event')) { - fieldType = 'event'; - } else if ($(this).hasClass('universal-field-btn')) { - fieldType = 'universal'; - } else if ($(this).hasClass('table-field-btn')) { - fieldType = 'table'; - } - - addTokenToTriggers(fieldname, $resultContainer, fieldType); - }); - - // Обработчик для кнопки выбора полей строк табличных частей - $modal.off('click', '.select-table-row-trigger-field').on('click', '.select-table-row-trigger-field', function() { - openTableRowFieldSelector(frm, $resultContainer, true); - }); - - // Показываем модальное окно - $modal.modal('show'); - - // Инициализируем перетаскивание - initTriggerDragAndDrop($modal, $resultContainer); - - // Настраиваем редактируемый контейнер - setupTriggerEditableContainer($resultContainer); - - // Фокусируемся на редактируемом поле - $resultContainer.focus(); -} - -function populateTriggerFieldsPanel($modal, frm, grouped_fields) { - const $container = $modal.find(".trigger-field-groups-container"); - $container.empty(); - - // Добавляем секцию предопределенных событий - const $predefinedEventsGroup = $(` -
-
Предопределенные события
-
-
- `); - - const $predefinedButtonsContainer = $predefinedEventsGroup.find('.trigger-field-buttons-container'); - - // Добавляем кнопки для предопределенных событий - const predefinedEvents = [ - { name: "При открытии документа", id: "При открытии документа" } - ]; - - predefinedEvents.forEach(event => { - const $eventBtn = $(` - - `); - - $predefinedButtonsContainer.append($eventBtn); - }); - - // Добавляем группу предопределенных событий в начало контейнера - $container.prepend($predefinedEventsGroup); - - // Добавляем секцию универсальных полей, если они есть - if (frm.doc.universal_fields_storage) { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - - if (Object.keys(universalFields).length > 0) { - const $universalFieldGroup = $(` -
-
Универсальные поля
-
-
- `); - - const $universalButtonsContainer = $universalFieldGroup.find('.trigger-field-buttons-container'); - - Object.keys(universalFields).forEach(fieldId => { - const field = universalFields[fieldId]; - - if (!field || !field.name) return; - - const $fieldBtn = $(` - - `); - - $universalButtonsContainer.append($fieldBtn); - }); - - $container.append($universalFieldGroup); - } - } - - // Добавляем обычные поля - Object.keys(grouped_fields).forEach(group => { - // Пропускаем пустые группы - if (!grouped_fields[group] || grouped_fields[group].length === 0) return; - - const $fieldGroup = $(` -
-
${group}
-
-
- `); - - const $buttonsContainer = $fieldGroup.find('.trigger-field-buttons-container'); - - grouped_fields[group].forEach(field => { - // Создаем кнопку поля с информацией о типе поля - const isTableField = group !== "Основные поля"; - const btnClass = isTableField ? "trigger-field-btn table-field-btn" : "trigger-field-btn"; - - const $fieldBtn = $(` - - `); - - $buttonsContainer.append($fieldBtn); - }); - - $container.append($fieldGroup); - }); - - // Добавляем группу для полей строк табличных частей - const $tableRowGroup = $(` -
-
Поля строк табличных частей
-
- -
-
- `); - - $container.append($tableRowGroup); - - // Добавляем стили для выделения различных типов триггеров - if (!$("#trigger-field-styles").length) { - $("head").append(` - - `); - } -} - -function addTokenToTriggers(fieldname, $resultContainer, fieldType = 'field') { - // Проверяем, не добавлено ли уже это поле - const existingTokens = $resultContainer.find(`.trigger-token[data-value="${fieldname}"]`); - if (existingTokens.length > 0) { - return; // Если поле уже добавлено, не добавляем его снова - } - - // Определяем класс токена в зависимости от типа поля - let tokenClass = "trigger-token field"; - - switch (fieldType) { - case 'event': - tokenClass = "trigger-token field highlight-event"; - break; - case 'universal': - tokenClass = "trigger-token field universal-field"; - break; - case 'table': - tokenClass = "trigger-token field table-field"; - break; - case 'table-row': - tokenClass = "trigger-token field table-row-field"; - break; - default: - tokenClass = "trigger-token field"; - } - - const $token = $(`${fieldname}×`); - $resultContainer.append($token).append(' '); - - // Переносим курсор в конец контейнера - setEndOfContentEditable($resultContainer[0]); - - // Добавляем обработчики для токенов - attachTriggerTokenEvents($resultContainer); -} - -function attachTriggerTokenEvents($container) { - // Удаление токена - $container.find('.token-remove').off('click').on('click', function(e) { - e.stopPropagation(); - $(this).parent().remove(); - }); - - // Перетаскивание токенов - $container.find('.trigger-token').each(function() { - $(this).attr('draggable', 'true'); - - $(this).off('dragstart').on('dragstart', function(e) { - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'trigger-token', - value: $(this).data('value'), - html: $(this)[0].outerHTML, - tokenClass: $(this).attr('class') - })); - window.draggedTriggerElement = this; - }); - - $(this).off('dragend').on('dragend', function() { - window.draggedTriggerElement = null; - }); - }); -} - -function initTriggerDragAndDrop($modal, $triggerContainer) { - // Очищаем предыдущие обработчики - $modal.find('.field-btn, .universal-field-btn, .trigger-field-btn').off('dragstart'); - $triggerContainer.off('dragover dragleave drop'); - - // Настраиваем перетаскивание существующих токенов в триггер-контейнере - $triggerContainer.find('.trigger-token').each(function() { - $(this).attr('draggable', 'true'); - - $(this).off('dragstart').on('dragstart', function(e) { - // Сохраняем ссылку на текущий элемент - window.draggedTriggerElement = this; - - // Добавляем данные для переноса - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'trigger-token', - value: $(this).data('value'), - html: $(this)[0].outerHTML, - tokenClass: $(this).attr('class') - })); - - // Добавляем класс перетаскивания - $(this).addClass('dragging'); - }); - - // При завершении перетаскивания - $(this).off('dragend').on('dragend', function() { - $(this).removeClass('dragging'); - window.draggedTriggerElement = null; - }); - }); - - // Настраиваем перетаскивание полей из панели в триггер-контейнер - $modal.find('.field-btn, .universal-field-btn, .trigger-field-btn').off('dragstart').on('dragstart', function(e) { - const fieldname = $(this).data('fieldname'); - const label = $(this).data('label'); - let fieldType = 'field'; - - if ($(this).hasClass('universal-field-btn')) { - fieldType = 'universal'; - } else if ($(this).hasClass('highlight-event')) { - fieldType = 'event'; - } else if ($(this).hasClass('table-field-btn')) { - fieldType = 'table'; - } - - const fieldData = { - type: 'trigger-field', - fieldname: fieldname, - label: label, - fieldType: fieldType - }; - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(fieldData)); - }); - - // Обработка dragover для триггер-контейнера - $triggerContainer.off('dragover').on('dragover', function(e) { - e.preventDefault(); - e.stopPropagation(); - $(this).addClass('drag-hover'); - - // Обновляем маркер сброса - updateDropMarker(e, $(this)); - }); - - // Обработка dragleave для триггер-контейнера - $triggerContainer.off('dragleave').on('dragleave', function(e) { - e.preventDefault(); - $(this).removeClass('drag-hover'); - $(this).find('.drop-marker').remove(); - }); - - // Обработка drop для триггер-контейнера - $triggerContainer.off('drop').on('drop', function(e) { - e.preventDefault(); - e.stopPropagation(); - $(this).removeClass('drag-hover'); - $(this).find('.drop-marker').remove(); - - // Обрабатываем сброшенные данные - handleTriggerDrop(e, $(this)); - }); -} - -function handleTriggerDrop(e, $container) { - try { - const data = JSON.parse(e.originalEvent.dataTransfer.getData('text/plain')); - - // Получаем позицию сброса - const x = e.originalEvent.clientX; - const y = e.originalEvent.clientY; - let position = findDropPosition(x, y, $container); - - // Обработка перетаскивания существующих токенов - if (data.type === 'trigger-token' && window.draggedTriggerElement) { - const $dragged = $(window.draggedTriggerElement); - - if (position.element) { - if (position.position === 'before') { - $(position.element).before($dragged); - $(position.element).before(' '); - } else { - $(position.element).after($dragged); - $(position.element).after(' '); - } - } else { - $container.append($dragged); - $container.append(' '); - } - - window.draggedTriggerElement = null; - return; - } - - // Обработка перетаскивания полей из панели - if (data.type === 'field' || data.type === 'trigger-field') { - // Проверяем, не добавлено ли уже это поле - const fieldname = data.fieldname; - const existingTokens = $container.find(`.trigger-token[data-value="${fieldname}"]`); - if (existingTokens.length > 0) { - return; // Если поле уже добавлено, не добавляем его снова - } - - // Определяем тип поля и создаем соответствующий токен - let fieldType = 'field'; - if (data.fieldType === 'universal') { - fieldType = 'universal'; - } else if (data.fieldType === 'table') { - fieldType = 'table'; - } else if (data.fieldType === 'table-row') { - fieldType = 'table-row'; - } else if (data.fieldType === 'event') { - fieldType = 'event'; - } - - // Создаем токен - const $token = createTriggerToken(fieldname, fieldType); - - // Вставляем токен в нужную позицию - if (position.element) { - if (position.position === 'before') { - $(position.element).before($token); - $(position.element).before(' '); - } else { - $(position.element).after($token); - $(position.element).after(' '); - } - } else { - $container.append($token); - $container.append(' '); - } - - // Инициализируем события для нового токена - initTriggerTokenEvents($token); - } - } catch (error) { - } -} - -function parseAndRenderTriggers($container, triggers) { - if (!triggers) return; - - // Разбиваем строку с триггерами по запятым - const triggerFields = triggers.split(',').map(field => field.trim()).filter(field => field); - - // Создаем токены для каждого поля - triggerFields.forEach(field => { - // Определяем тип триггера - let fieldType = 'field'; - - if (isPredefinedEvent(field)) { - fieldType = 'event'; - } else if (field.startsWith('univ(')) { - fieldType = 'universal'; - } else if (field.includes('_') && !field.includes('_row_')) { - // Проверяем, является ли это полем табличной части - const parts = field.split('_'); - if (parts.length >= 2) { - fieldType = 'table'; - } - } else if (field.includes('_row_')) { - fieldType = 'table-row'; - } - - addTokenToTriggers(field, $container, fieldType); - }); -} - -function getTriggerFieldsText($container) { - // Собираем значения всех токенов - const triggerFields = []; - - $container.find('.trigger-token').each(function() { - triggerFields.push($(this).data('value')); - }); - - // Объединяем их через запятую - return triggerFields.join(', '); -} - -function setupTriggerEditableContainer($container) { - // Обработчик нажатия клавиш в контейнере триггеров - $container.off('keydown').on('keydown', function(e) { - // Если нажата клавиша Delete или Backspace - if (e.keyCode === 8 || e.keyCode === 46) { - const selection = window.getSelection(); - - if (selection.rangeCount > 0) { - const range = selection.getRangeAt(0); - - if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) { - // Текстовый узел - обычное удаление - return true; - } else { - // Возможно, выделен токен - const $selectedNode = $(range.commonAncestorContainer); - - if ($selectedNode.hasClass('trigger-token') || $selectedNode.parents('.trigger-token').length) { - // Удаляем токен - const $token = $selectedNode.hasClass('trigger-token') ? - $selectedNode : $selectedNode.parents('.trigger-token'); - $token.remove(); - e.preventDefault(); - return false; - } - } - } - } - - return true; - }); - -// Обработчик вставки текста - $container.off('paste').on('paste', function(e) { - e.preventDefault(); - - // Получаем вставляемый текст - let text = ''; - if (e.clipboardData || e.originalEvent.clipboardData) { - text = (e.clipboardData || e.originalEvent.clipboardData).getData('text/plain'); - } else if (window.clipboardData) { - text = window.clipboardData.getData('Text'); - } - - // Разбиваем вставляемый текст по запятым и добавляем как токены - if (text) { - const fields = text.split(',').map(field => field.trim()).filter(field => field); - - fields.forEach(field => { - // Определяем тип триггера - let fieldType = 'field'; - - if (isPredefinedEvent(field)) { - fieldType = 'event'; - } else if (field.startsWith('univ(')) { - fieldType = 'universal'; - } else if (field.includes('_') && !field.includes('_row_')) { - // Проверяем, является ли это полем табличной части - const parts = field.split('_'); - if (parts.length >= 2) { - fieldType = 'table'; - } - } else if (field.includes('_row_')) { - fieldType = 'table-row'; - } - - // Проверяем, не добавлено ли уже это поле - const existingTokens = $container.find(`.trigger-token[data-value="${field}"]`); - if (existingTokens.length === 0) { - addTokenToTriggers(field, $container, fieldType); - } - }); - } - }); -} - -// Функции для работы с универсальными полями -function addUniversalFieldsSection(wrapper) { - const universalSection = $('
'); - universalSection.append('

Универсальные поля

'); - universalSection.append('
'); - wrapper.append(universalSection); -} - -function initUniversalFields(frm) { - // Если хранилище универсальных полей не существует, создаем его - if (!frm.doc.universal_fields_storage) { - frm.set_value("universal_fields_storage", "{}"); - } - - // Инициализируем обработчики событий - attachUniversalFieldsEvents(frm); - - // Для использования стилей - addUniversalFieldsStyles(); -} - -function addUniversalFieldsStyles() { - if ($('#universal-fields-styles').length) return; - - const styles = ` - `; - - $('head').append(styles); -} - -function attachUniversalFieldsEvents(frm) { - // Сначала удаляем существующие обработчики событий - $(frm.wrapper).off('click', '.add-universal-field'); - $(frm.wrapper).off('click', '.edit-universal-field'); - $(frm.wrapper).off('click', '.delete-universal-field'); - - // Обработчик нажатия на кнопку "Добавить поле" - $(frm.wrapper).on('click', '.add-universal-field', function() { - openUniversalFieldModal(frm); - }); - - // Обработчик для клика по кнопке редактирования - $(frm.wrapper).on('click', '.edit-universal-field', function(e) { - e.stopPropagation(); - const fieldId = $(this).data('field-id'); - editUniversalField(frm, fieldId); - }); - - // Обработчик для клика по кнопке удаления - $(frm.wrapper).on('click', '.delete-universal-field', function(e) { - e.stopPropagation(); - const fieldId = $(this).data('field-id'); - deleteUniversalField(frm, fieldId); - }); -} - -function openUniversalFieldModal(frm, fieldId = null) { - // Закрываем предыдущее окно, если оно открыто - if ($("#universal_fields_modal").is(":visible")) { - $("#universal_fields_modal").modal('hide'); - } - - // Получаем значения для редактирования (если указан fieldId) - let fieldValues = {}; - if (fieldId) { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - fieldValues = universalFields[fieldId] || {}; - } - - // Создаем диалог - const d = new frappe.ui.Dialog({ - title: fieldId ? 'Редактирование универсального поля' : 'Создание универсального поля', - fields: [ - { - label: 'Название поля', - fieldname: 'name', - fieldtype: 'Data', - reqd: 1, - default: fieldValues.name || '' - }, - { - label: 'Тип документа', - fieldname: 'doctype', - fieldtype: 'Link', - options: 'DocType', - reqd: 1, - default: fieldValues.doctype || '', - filters: { - "istable": 0, - "issingle": 0 - }, - onchange: function() { - const selectedDoctype = d.get_value('doctype'); - - if (selectedDoctype) { - // Очищаем таблицу фильтров перед загрузкой новых полей - if (d.fields_dict.filters && d.fields_dict.filters.grid) { - d.fields_dict.filters.grid.grid_rows.forEach(row => { - row.remove(); - }); - } - - // Загружаем поля выбранного DocType - frappe.model.with_doctype(selectedDoctype, function() { - const meta = frappe.get_meta(selectedDoctype); - - if (!meta) { - frappe.msgprint({ - title: __('Ошибка'), - message: __(`DocType ${selectedDoctype} не найден`), - indicator: 'red' - }); - return; - } - - // Получаем все доступные поля, а не только числовые - const excluded_types = [ - "Tab Break", "Column Break", "Button", "Section Break", "Column Break", - "HTML", "Table", "Attach", "Attach Image", "Code", "Text Editor", "Image", - "Heading", "Fold", "Table MultiSelect" - ]; - - const availableFields = meta.fields.filter(field => - !excluded_types.includes(field.fieldtype) - ); - - if (availableFields.length === 0) { - frappe.msgprint({ - title: __('Предупреждение'), - message: __(`В DocType ${selectedDoctype} не найдено подходящих полей`), - indicator: 'orange' - }); - return; - } - - // Обновляем опции для поля fieldname - const options = availableFields.map(field => ({ - label: field.label || field.fieldname, - value: field.fieldname - })); - - d.set_df_property('fieldname', 'options', options); - - // Если есть значение поля для редактирования, выбираем его - if (fieldValues.fieldname) { - d.set_value('fieldname', fieldValues.fieldname); - } - - // Обновляем опции для поля фильтра - const filterFieldOptions = meta.fields - .filter(field => - !excluded_types.includes(field.fieldtype) - ) - .map(field => ({ - label: field.label || field.fieldname, - value: field.fieldname - })); - - // Сохраняем информацию о полях для использования в фильтрах - d.docFields = meta.fields; - window.docFields = meta.fields; - - // Обновляем опции для фильтров - if (d.fields_dict.filters && d.fields_dict.filters.grid) { - d.fields_dict.filters.grid.update_docfield_property('field', 'options', filterFieldOptions); - } - - // Если редактируем существующее поле и есть фильтры, добавляем их - if (fieldId && fieldValues.filters && fieldValues.filters.length) { - setTimeout(() => { - // Вручную добавляем каждый фильтр - fieldValues.filters.forEach(filter => { - if (!filter || !filter.field) return; - - // Добавляем строку в таблицу фильтров - d.fields_dict.filters.grid.add_new_row(); - const lastRow = d.fields_dict.filters.grid.grid_rows[d.fields_dict.filters.grid.grid_rows.length - 1]; - - // Устанавливаем значения фильтра - lastRow.doc.field = filter.field; - lastRow.doc.operator = filter.operator || '='; - lastRow.doc.value = filter.value || ''; - - // Дополнительные свойства из оригинального фильтра - if (filter.__is_special_ay_field) { - lastRow.doc.__is_special_ay_field = true; - } - - // Обновляем отображение строки - lastRow.refresh(); - - // Находим информацию о поле для настройки типа значения - const fieldInfo = d.docFields.find(f => f.fieldname === filter.field); - if (fieldInfo) { - setTimeout(() => { - handleFilterFieldChange(d, lastRow, fieldInfo); - }, 100); - } - }); - - // Обновляем всю таблицу - d.fields_dict.filters.grid.refresh(); - }, 500); - } - }); - } - } - }, - { - label: 'Поле документа', - fieldname: 'fieldname', - fieldtype: 'Select', - options: [], - reqd: 1, - default: fieldValues.fieldname || '' - }, - { - label: 'Фильтры', - fieldname: 'filters', - fieldtype: 'Table', - fields: [ - { - label: 'Поле', - fieldname: 'field', - fieldtype: 'Select', - options: [], - reqd: 1, - in_list_view: 1, - columns: 4, - change: function() { - const grid_row = this.grid_row; - const fieldname = this.get_value(); - - if (d.docFields && fieldname) { - const fieldInfo = d.docFields.find(f => f.fieldname === fieldname); - - if (fieldInfo) { - // Изменяем тип поля значения - handleFilterFieldChange(d, grid_row, fieldInfo); - } - } - } - }, - { - label: 'Оператор', - fieldname: 'operator', - fieldtype: 'Select', - options: [ - {label: '=', value: '='}, - {label: '!=', value: '!='}, - {label: '>', value: '>'}, - {label: '>=', value: '>='}, - {label: '<', value: '<'}, - {label: '<=', value: '<='}, - {label: 'Содержит', value: 'like'}, - {label: 'Не содержит', value: 'not like'}, - {label: 'В списке', value: 'in'}, - {label: 'Не в списке', value: 'not in'} - ], - default: '=', - reqd: 1, - in_list_view: 1, - columns: 3 - }, - { - label: 'Значение', - fieldname: 'value', - fieldtype: 'Data', - in_list_view: 1, - columns: 4 - } - ], - default: [] - } - ], - primary_action_label: fieldId ? 'Обновить' : 'Создать', - primary_action: function() { - const values = d.get_values(); - - // Проверяем, что все обязательные поля заполнены - if (!values.name || !values.doctype || !values.fieldname) { - frappe.msgprint({ - title: __('Внимание'), - message: __('Заполните все обязательные поля'), - indicator: 'orange' - }); - return; - } - - // Сохраняем данные - saveUniversalField(frm, values, fieldId); - - d.hide(); - } - }); - - // Безопасно инициализируем обработчики для строк фильтров - try { - if (d.fields_dict.filters && d.fields_dict.filters.grid) { - const filterField = d.fields_dict.filters.grid.get_field('field'); - if (filterField && typeof filterField.refresh === 'function') { - filterField.refresh(); - } - - // Добавляем обработчик на событие добавления строки - d.fields_dict.filters.grid.on('row-added', function(row) { - if (d.docFields) { - window.docFields = d.docFields; - } - }); - } - } catch (error) { - } - - // Если редактируем существующее поле, вызываем событие изменения doctype для загрузки полей - if (fieldId && fieldValues.doctype) { - // Даем время на инициализацию компонентов - setTimeout(() => { - d.fields_dict.doctype.df.onchange.call(d.fields_dict.doctype); - }, 200); - } - - // Показываем диалог - d.show(); -} - -function handleFilterFieldChange(dialog, grid_row, fieldInfo) { - - // Проверяем, что у нас есть все необходимые объекты - if (!dialog || !grid_row || !fieldInfo) { - return; - } - - // Получаем текущее значение - let currentValue = undefined; - try { - if (grid_row.doc) { - currentValue = grid_row.doc.value; - } - } catch (error) { - } - - // Определяем новый тип поля в зависимости от типа выбранного поля - let fieldtype = 'Data'; - let options = ''; - - switch (fieldInfo.fieldtype) { - case 'Select': - fieldtype = 'Select'; - options = fieldInfo.options; - break; - - case 'Link': - fieldtype = 'Link'; - options = fieldInfo.options; - break; - - case 'Check': - fieldtype = 'Check'; - break; - - case 'Date': - fieldtype = 'Select'; // Используем Select для показа выбора полей даты - - // Получаем target_doctype из основной формы - const target_doctype = cur_frm.doc.target_doctype; - - if (target_doctype) { - - // Загружаем поля целевого DocType - try { - frappe.model.with_doctype(target_doctype, function() { - const targetMeta = frappe.get_meta(target_doctype); - if (targetMeta) { - // Фильтруем только поля типа Date и специальные поля ay/il из целевого DocType - const targetDateFields = targetMeta.fields.filter(field => - field.fieldtype === 'Date' || - field.fieldname === 'ay' || - field.fieldname === 'il' || - field.fieldname === 'rüb' - ); - - // Проверяем, есть ли специальные поля ay и il одновременно - 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) { - // Создаем опции для полей с датой - options = targetDateFields.filter(field => field.fieldtype === 'Date').map(field => ({ - label: field.label || field.fieldname, - value: `doc.${field.fieldname}` - })); - - // Если есть оба специальных поля, добавляем опцию для выбора месяца - if (hasAyField && hasIlField) { - options.push({ - label: 'Месяц (Ay)', - value: 'doc.ay' - }); - } - - if (hasRubField && hasIlField) { - options.push({ - label: 'Квартал (Rüb)', - value: 'doc.rüb' - }); - } - - // Добавляем опцию для пользовательского ввода - options.push({ - label: 'Указать дату...', - value: 'custom' - }); - - // Обновляем свойства поля значения - if (dialog.fields_dict.filters && dialog.fields_dict.filters.grid) { - dialog.fields_dict.filters.grid.update_docfield_property('value', 'fieldtype', fieldtype); - dialog.fields_dict.filters.grid.update_docfield_property('value', 'options', options); - - // Обновляем контрол - if (grid_row && typeof grid_row.refresh === 'function') { - grid_row.refresh(); - } - } - } else { - fieldtype = 'Date'; // Если нет полей с датой, используем обычный Date - } - } else { - fieldtype = 'Date'; // Если не удалось получить метаданные, используем обычный Date - } - }); - } catch (error) { - fieldtype = 'Date'; // В случае ошибки используем обычный Date - } - } else { - fieldtype = 'Date'; // Если target_doctype не указан, используем обычный Date - } - break; - - case 'Datetime': - fieldtype = 'Datetime'; - break; - - case 'Int': - case 'Float': - case 'Currency': - fieldtype = 'Float'; - break; - - default: - fieldtype = 'Data'; - } - - try { - // Обновляем свойства поля значения (только для не-Date полей, для Date это делается в асинхронном колбэке) - if (fieldInfo.fieldtype !== 'Date' && dialog.fields_dict.filters && dialog.fields_dict.filters.grid) { - dialog.fields_dict.filters.grid.update_docfield_property('value', 'fieldtype', fieldtype); - dialog.fields_dict.filters.grid.update_docfield_property('value', 'options', options); - } - - // Обработчик для опции "Указать дату..." и опции "Месяц (Ay)" (для типа Date) - if (fieldInfo.fieldtype === 'Date' && fieldtype === 'Select') { - // Добавляем обработчик для изменения селекта поля даты - try { - // Безопасно устанавливаем обработчик onchange - if (dialog.fields_dict.filters.grid.fields_dict && dialog.fields_dict.filters.grid.fields_dict.value) { - const valueField = dialog.fields_dict.filters.grid.fields_dict.value; - if (valueField.df) { - valueField.df.onchange = function(e) { - try { - // Для случаев, когда this.get_value не существует или не работает - let value = ''; - - // Пробуем разные способы получить значение - if (this.get_value && typeof this.get_value === 'function') { - value = this.get_value(); - } else if (e && e.target) { - value = e.target.value; - } else if (grid_row && grid_row.doc) { - value = grid_row.doc.value; - } - - if (value === 'custom') { - // Переключаем на ручной ввод даты - dialog.fields_dict.filters.grid.update_docfield_property('value', 'fieldtype', 'Date'); - dialog.fields_dict.filters.grid.update_docfield_property('value', 'options', ''); - - // Обновляем контрол - if (grid_row && typeof grid_row.refresh_field === 'function') { - grid_row.refresh_field('value'); - } - } - else if (value === 'doc.ay') { - - // Для doc.ay не меняем тип поля, только запоминаем в grid_row, что это специальный случай - if (grid_row && grid_row.doc) { - grid_row.doc.__is_special_ay_field = true; - } - } - else if (value === 'doc.rüb') { - // Помечаем в data-атрибуте, что это специальное поле rüb - if (grid_row && grid_row.doc) { - grid_row.doc.__is_special_rub_field = true; - } - } - } catch (error) { - } - }; - } - } - } catch (error) { - } - } - } catch (error) { - } - - try { - // Пересоздаем контрол (только для не-Date полей, для Date это делается в асинхронном колбэке) - if (fieldInfo.fieldtype !== 'Date' && grid_row && typeof grid_row.refresh === 'function') { - grid_row.refresh(); - } - - // Устанавливаем значение обратно - if (currentValue !== undefined && grid_row && grid_row.doc) { - grid_row.doc.value = currentValue; - if (typeof grid_row.refresh_field === 'function') { - grid_row.refresh_field('value'); - } - } - } catch (error) { - } -} - -function loadDoctypes() { - return new Promise((resolve) => { - // Инициализируем список DocType пустым массивом - window.allDoctypes = []; - - frappe.call({ - method: "frappe.desk.search.search_link", - args: { - doctype: "DocType", - txt: "", - filters: {"issingle": 0} - }, - callback: function(r) { - if (r.results && r.results.length) { - // Сохраняем полный список DocType - window.allDoctypes = r.results.map(doc => doc.value || doc.name); - } - initDoctypeAutocomplete(); - resolve(); - } - }); - }); -} - -function initDoctypeAutocomplete() { - const $input = $("#universal_field_doctype"); - const $dropdown = $(".doctype-dropdown"); - - // Обработчик ввода для поиска DocType - $input.off('input').on('input', function() { - const searchTerm = $(this).val().toLowerCase(); - - if (searchTerm.length < 1) { - $dropdown.hide(); - return; - } - - // Делаем поиск через API для каждого ввода - frappe.call({ - method: "frappe.desk.search.search_link", - args: { - doctype: "DocType", - txt: searchTerm, - filters: {"issingle": 0} - }, - callback: function(r) { - if (r.results && r.results.length) { - $dropdown.empty(); - r.results.forEach(item => { - $dropdown.append(``); - }); - $dropdown.show(); - } else { - $dropdown.empty(); - $dropdown.append(``); - $dropdown.show(); - } - } - }); - }); - - // Обработчик выбора DocType из выпадающего списка - $(document).off('click', '.doctype-item').on('click', '.doctype-item', function() { - const itemText = $(this).text(); - - // Проверяем, не является ли это сообщением об ошибке - if (itemText === 'DocType не найден' || itemText === 'Загрузка списка DocType...') { - return; - } - - const selectedDoctype = itemText; - $input.val(selectedDoctype); - $dropdown.hide(); - - // Загружаем поля выбранного DocType - loadDoctypeFields(selectedDoctype); - }); -} - -function loadDoctypeFields(doctype) { - const $modal = $("#universal_fields_modal"); - const $fieldsSelect = $modal.find('#universal_field_fieldname'); - const $filterFieldsSelect = $modal.find('.filter-field'); - - // Показываем индикатор загрузки - $fieldsSelect.html(''); - - // Получаем поля DocType - frappe.model.with_doctype(doctype, function() { - const meta = frappe.get_meta(doctype); - - if (!meta) { - frappe.msgprint({ - title: __('Ошибка'), - message: __(`DocType ${doctype} не найден`), - indicator: 'red' - }); - $fieldsSelect.html(''); - $modal.find('.universal-fields-container').hide(); - return; - } - - $fieldsSelect.empty(); - $fieldsSelect.append(''); - - $filterFieldsSelect.empty(); - $filterFieldsSelect.append(''); - - // Фильтруем поля только для числовых типов для основного поля - const numericTypes = ['Int', 'Float', 'Currency']; - - // Для фильтров разрешаем гораздо больше типов - const filterTypes = [ - 'Int', 'Float', 'Currency', 'Data', 'Text', 'Date', 'Datetime', - 'Check', 'Select', 'Link', 'Small Text', 'Read Only' - ]; - - // Сохраняем все поля для использования в фильтрах - window.docFields = meta.fields; - - let hasNumericFields = false; - - meta.fields.forEach(field => { - // Для основного поля - только числовые типы - if (field.fieldname && numericTypes.includes(field.fieldtype)) { - $fieldsSelect.append(``); - hasNumericFields = true; - } - - // Для фильтров - более широкий набор типов - if (field.fieldname && filterTypes.includes(field.fieldtype)) { - $filterFieldsSelect.append(``); - } - }); - - // Показываем контейнер полей только если найдены числовые поля - if (hasNumericFields) { - $modal.find('.universal-fields-container').show(); - } else { - $modal.find('.universal-fields-container').hide(); - frappe.msgprint({ - title: __('Предупреждение'), - message: __(`В DocType ${doctype} не найдено числовых полей`), - indicator: 'orange' - }); - } - }); -} - -function addFilterRow($modal) { - const $filters = $modal.find('.universal-filters'); - const doctype = $modal.find('#universal_field_doctype').val(); - - if (!doctype) return; - - const $newRow = $(` -
-
-
- -
-
- -
-
- -
-
- -
-
-
- `); - - // Копируем опции полей из первой строки фильтра - const $firstFilterSelect = $modal.find('.filter-field').first(); - $newRow.find('.filter-field').html($firstFilterSelect.html()); - - $filters.append($newRow); - - // Добавляем обработчик изменения поля фильтра - $newRow.find('.filter-field').on('change', function() { - const fieldname = $(this).val(); - const fieldInfo = getFieldInfo(fieldname); - updateFilterValueField($(this).closest('.filter-row'), fieldInfo); - }); -} - -function getFieldInfo(fieldname) { - if (!window.docFields || !fieldname) return null; - - return window.docFields.find(field => field.fieldname === fieldname); -} - -function updateFilterValueField($filterRow, fieldInfo) { - const $valueContainer = $filterRow.find('.filter-value-container'); - const $operator = $filterRow.find('.filter-operator'); - - // Если информация о поле отсутствует, возвращаем обычное текстовое поле - if (!fieldInfo) { - $valueContainer.html(''); - return; - } - - // В зависимости от типа поля создаем соответствующий элемент ввода - switch (fieldInfo.fieldtype) { - 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) { - // Фильтруем поля даты и специальные поля ay/il - const targetDateFields = targetMeta.fields.filter(field => - field.fieldtype === 'Date' || - field.fieldname === 'ay' || - field.fieldname === 'il' - ); - - // Проверяем, есть ли специальные поля ay и il одновременно - const hasAyField = targetDateFields.some(field => field.fieldname === 'ay'); - const hasIlField = targetDateFields.some(field => field.fieldname === 'il'); - - if (targetDateFields.length > 0) { - // Создаем селект с опциями из полей с датой - let selectHtml = ''; - - $valueContainer.html(selectHtml); - - // Добавляем обработчик для опции "Указать дату..." - $valueContainer.find('.filter-value').on('change', function() { - const selectedValue = $(this).val(); - - if (selectedValue === 'custom') { - $valueContainer.html(''); - } - else if (selectedValue === 'doc.ay') { - // Помечаем в data-атрибуте, что это специальное поле ay - $(this).attr('data-is-special-ay-field', 'true'); - } - }); - } else { - $valueContainer.html(''); - } - } else { - $valueContainer.html(''); - } - }); - } else { - $valueContainer.html(''); - } - break; - - case 'Datetime': - $valueContainer.html(''); - break; - - case 'Select': - if (fieldInfo.options) { - let selectHtml = ''; - $valueContainer.html(selectHtml); - } else { - $valueContainer.html(''); - } - break; - - case 'Link': - if (fieldInfo.options) { - const targetDoctype = fieldInfo.options; - $valueContainer.html(` -
- - -
- `); - - // Инициализация автодополнения для Link полей - initLinkAutocomplete($valueContainer.find('.filter-value')); - } else { - $valueContainer.html(''); - } - break; - - case 'Check': - $valueContainer.html(` - - `); - break; - - case 'Int': - $valueContainer.html(''); - break; - - case 'Float': - case 'Currency': - $valueContainer.html(''); - break; - - default: - $valueContainer.html(''); - } - - // Обновляем доступные операторы в зависимости от типа поля - updateOperators($operator, fieldInfo.fieldtype); -} - -function initLinkAutocomplete($input) { - const targetDoctype = $input.data('link-target'); - if (!targetDoctype) return; - - $input.on('focus', function() { - if (!$input.data('initialized')) { - $input.data('initialized', true); - - // Загружаем значения из целевого DocType - frappe.call({ - method: "frappe.desk.search.search_link", - args: { - doctype: targetDoctype, - txt: "" - }, - callback: function(r) { - if (r.results && r.results.length) { - window.linkOptions = r.results.map(item => item.value); - } - } - }); - } - }); - - $input.on('input', function() { - const searchTerm = $(this).val().toLowerCase(); - const $dropdown = $(this).siblings('.link-dropdown'); - - if (!window.linkOptions || searchTerm.length < 1) { - $dropdown.hide(); - return; - } - - // Фильтруем значения по поисковому запросу - const filteredOptions = window.linkOptions.filter( - option => option.toLowerCase().includes(searchTerm) - ); - - // Отображаем результаты в выпадающем списке - if (filteredOptions.length > 0) { - $dropdown.empty(); - filteredOptions.forEach(option => { - $dropdown.append(``); - }); - $dropdown.show(); - } else { - $dropdown.hide(); - } - }); - - // Обработчик выбора значения из выпадающего списка - $(document).on('click', '.link-item', function() { - const selectedValue = $(this).text(); - $(this).closest('.awesomplete').find('.filter-value').val(selectedValue); - $(this).closest('.link-dropdown').hide(); - }); -} - -function updateOperators($operator, fieldtype) { - $operator.empty(); - - // Базовые операторы для всех типов - $operator.append(''); - $operator.append(''); - - // Дополнительные операторы в зависимости от типа - if (['Int', 'Float', 'Currency', 'Date', 'Datetime'].includes(fieldtype)) { - $operator.append(''); - $operator.append(''); - $operator.append(''); - $operator.append(''); - } - - if (['Data', 'Text', 'Small Text', 'Link', 'Select'].includes(fieldtype)) { - $operator.append(''); - $operator.append(''); - } - - if (['Link', 'Select'].includes(fieldtype)) { - $operator.append(''); - $operator.append(''); - } -} - -function validateUniversalFieldForm($modal) { - const name = $modal.find('#universal_field_name').val().trim(); - const doctype = $modal.find('#universal_field_doctype').val(); - const fieldname = $modal.find('#universal_field_fieldname').val(); - - // Проверяем, что все обязательные поля заполнены - const isValid = name && doctype && fieldname; - $modal.find('.save-universal-field').prop('disabled', !isValid); -} - -function saveUniversalField(frm, values, fieldId = null) { - // Используем имя поля в качестве идентификатора вместо случайной строки - const newFieldId = fieldId || values.name.toLowerCase().replace(/[^a-z0-9_]/g, '_'); - - try { - let universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - - // Проверяем, не существует ли уже поле с таким именем (кроме текущего редактируемого) - const existingField = Object.keys(universalFields).find(key => - key !== newFieldId && universalFields[key].name === values.name - ); - - if (existingField) { - frappe.msgprint({ - title: __('Внимание'), - message: __('Поле с таким именем уже существует.'), - indicator: 'orange' - }); - return; - } - - // Получаем тип поля - let fieldtype = ''; - if (values.doctype && values.fieldname) { - const meta = frappe.get_meta(values.doctype); - if (meta) { - const field = meta.fields.find(f => f.fieldname === values.fieldname); - if (field) { - fieldtype = field.fieldtype; - } - } - } - - // Проверяем фильтры на наличие специальных полей ay/il - if (values.filters && Array.isArray(values.filters)) { - values.filters.forEach(filter => { - if (typeof filter.value === 'string' && filter.value === 'doc.ay') { - filter.__is_special_ay_field = true; - } - }); - } - - // Сохраняем информацию о поле в идентификатор, соответствующий имени поля - universalFields[newFieldId] = { - name: values.name, - doctype: values.doctype, - fieldname: values.fieldname, - fieldtype: fieldtype, - filters: values.filters || [] - }; - - frm.set_value("universal_fields_storage", JSON.stringify(universalFields)); - - frappe.show_alert({ - message: __('Универсальное поле сохранено'), - indicator: 'green' - }); - - renderUniversalFields(frm); - } catch (error) { - frappe.msgprint({ - title: __('Ошибка'), - message: __('Не удалось сохранить универсальное поле.'), - indicator: 'red' - }); - } -} - -function fillFormForEditing(frm, fieldId, $modal) { - try { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - const field = universalFields[fieldId]; - - if (!field) { - return; - } - - // Заполняем базовые поля - $modal.find('#universal_field_name').val(field.name); - $modal.find('#universal_field_doctype').val(field.doctype); - - // Загружаем поля выбранного DocType и затем выбираем нужное поле - loadDoctypeFields(field.doctype); - - // Настраиваем обработчик для загрузки полей doctype - const loadFieldsWatcher = setInterval(function() { - const $fieldnameSelect = $modal.find('#universal_field_fieldname'); - - if ($fieldnameSelect.find('option').length > 1) { - clearInterval(loadFieldsWatcher); - - $fieldnameSelect.val(field.fieldname); - $modal.find('.universal-fields-container').show(); - $modal.find('.universal-filter-container').show(); - - // Очищаем существующие фильтры - $modal.find('.universal-filters').empty(); - - // Добавляем сохраненные фильтры - if (field.filters && field.filters.length > 0) { - field.filters.forEach((filter, index) => { - // Добавляем новую строку фильтра - if (index > 0) { - addFilterRow($modal); - } - - const $filterRow = $modal.find('.filter-row').eq(index); - - // Устанавливаем значения фильтра с небольшой задержкой для загрузки полей - setTimeout(() => { - const $fieldSelect = $filterRow.find('.filter-field'); - $fieldSelect.val(filter.field); - - // Обновляем поле значения в зависимости от типа поля - const fieldInfo = getFieldInfo(filter.field); - if (fieldInfo) { - updateFilterValueField($filterRow, fieldInfo); - - // Устанавливаем оператор и значение - $filterRow.find('.filter-operator').val(filter.operator); - $filterRow.find('.filter-value').val(filter.value); - } - }, 200); - }); - } else { - // Добавляем пустую строку фильтра, если нет сохраненных - addFilterRow($modal); - } - - // Валидируем форму - validateUniversalFieldForm($modal); - } - }, 100); - - // Ограничиваем время ожидания 5 секундами - setTimeout(function() { - clearInterval(loadFieldsWatcher); - }, 5000); - - } catch (error) { - } -} - -function deleteUniversalField(frm, fieldId) { - frappe.confirm( - 'Вы уверены, что хотите удалить это универсальное поле?', - function() { - try { - let universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - delete universalFields[fieldId]; - frm.set_value("universal_fields_storage", JSON.stringify(universalFields)); - - // Перерисовываем список полей - renderUniversalFields(frm); - - frappe.show_alert({ - message: __('Универсальное поле удалено'), - indicator: 'green' - }); - } catch (error) { - frappe.msgprint({ - title: __('Ошибка'), - message: __('Не удалось удалить универсальное поле.'), - indicator: 'red' - }); - } - } - ); -} - -function editUniversalField(frm, fieldId) { - openUniversalFieldModal(frm, fieldId); -} - -// Вспомогательная функция для получения доступных префиксов таблиц -function getTablePrefixes() { - // Предопределенный список известных префиксов таблиц - const knownPrefixes = ["items", "taxes", "packed_items", "payments", "advances", "timesheets", "payment_schedule", "sales_team", "aksiz_test_table"]; - - // Добавляем дополнительные префиксы из текущего DOM, если доступно - try { - // Ищем таблицы в DOM - let domPrefixes = []; - $("div.form-section div.form-section-title").each(function() { - const sectionTitle = $(this).text().trim(); - if (sectionTitle && sectionTitle !== "Основные поля") { - // Преобразуем название секции в возможный префикс таблицы - // Например: "Items (items)" -> "items" - const match = sectionTitle.match(/\(([^)]+)\)/); - if (match) { - domPrefixes.push(match[1]); - } else { - // Иначе используем название секции, преобразованное в нижний регистр без пробелов - domPrefixes.push(sectionTitle.toLowerCase().replace(/\s+/g, '_')); - } - } - }); - - // Объединяем найденные префиксы с известными - return [...new Set([...knownPrefixes, ...domPrefixes])]; - } catch (e) { - return knownPrefixes; - } -} - -function renderUniversalFields(frm) { - const $container = $(frm.wrapper).find('.universal-fields-list'); - $container.empty(); - - try { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - - if (Object.keys(universalFields).length === 0) { - $container.append('
Нет универсальных полей. Нажмите "Добавить поле" для создания.
'); - return; - } - - Object.keys(universalFields).forEach(fieldId => { - const field = universalFields[fieldId]; - - const $fieldButton = $(` -
- -
- - -
-
- `); - - // Показываем/скрываем кнопки действий при наведении - $fieldButton.hover( - function() { $(this).find('.field-actions').show(); }, - function() { $(this).find('.field-actions').hide(); } - ); - - $container.append($fieldButton); - }); - - // Инициализируем drag-n-drop для универсальных полей - $container.find('.universal-field-btn').each(function() { - $(this).attr('draggable', 'true'); - - $(this).off('dragstart').on('dragstart', function(e) { - const fieldId = $(this).data('field-id'); - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - const field = universalFields[fieldId]; - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'universal-field', - fieldId: fieldId, - name: field.name - })); - }); - }); - } catch (error) { - $container.append('
Ошибка при загрузке универсальных полей.
'); - } -} - -function delete_client_script_for_doctype(doctype) { - if (!doctype) { - return; - } - - frappe.call({ - method: "frappe.client.get_list", - args: { - doctype: "Client Script", - filters: { dt: doctype }, - fields: ["name"] - }, - callback: function(r) { - if (r.message && r.message.length > 0) { - const script_names = r.message.map(script => script.name); - - // Удаляем каждый найденный скрипт - script_names.forEach(script_name => { - frappe.call({ - method: "frappe.client.delete", - args: { - doctype: "Client Script", - name: script_name - }, - callback: function(response) { - if (response.exc) { - } else { - } - } - }); - }); - } else { - } - } - }); -} - -// Функция для обработки изменения поля в таблице фильтров -function setupFilterRowHandlers(d) { - // Обработчик изменения поля в фильтре - d.fields_dict.filters.grid.get_visible_rows().forEach(row => { - setupFilterFieldHandler(d, row); - }); -} - -// Функция для настройки обработчика изменения поля в строке фильтра -function setupFilterFieldHandler(d, row) { - if (!row.doc || row.doc.__processed) return; - - // Отмечаем строку как обработанную - row.doc.__processed = true; - - // Находим селект поля и добавляем обработчик - $(row.wrapper).find('select[data-fieldname="field"]').on('change', function() { - const fieldname = $(this).val(); - changeFilterValueFieldType(d, row, fieldname); - }); - - // Обрабатываем текущее значение, если оно уже установлено - if (row.doc.field) { - changeFilterValueFieldType(d, row, row.doc.field); - } -} - -// Функция для изменения типа поля значения в зависимости от выбранного поля -function changeFilterValueType(grid_row, fieldInfo) { - if (!grid_row || !fieldInfo) return; - - // Получаем текущее значение - const currentValue = grid_row.doc.value; - - // Определяем новый тип поля в зависимости от типа выбранного поля - let fieldtype = 'Data'; - let options = ''; - - switch (fieldInfo.fieldtype) { - case 'Select': - fieldtype = 'Select'; - options = fieldInfo.options; - break; - - case 'Link': - fieldtype = 'Link'; - options = fieldInfo.options; - break; - - case 'Check': - fieldtype = 'Check'; - break; - - case 'Date': - fieldtype = 'Date'; - break; - - case 'Datetime': - fieldtype = 'Datetime'; - break; - - case 'Int': - case 'Float': - case 'Currency': - fieldtype = 'Float'; - break; - - default: - fieldtype = 'Data'; - } - - // Обновляем определение поля значения - grid_row.grid.meta.fields.forEach(field => { - if (field.fieldname === 'value') { - field.fieldtype = fieldtype; - field.options = options; - } - }); - - // Обновляем колонку значений для текущего DocType - grid_row.grid.grid_rows.forEach(row => { - // Получаем контрол поля значения - const value_field = row.columns_list.find(col => col.df.fieldname === 'value'); - if (value_field) { - // Пересоздаем контрол - value_field.df.fieldtype = fieldtype; - value_field.df.options = options; - row.refresh_field('value'); - } - }); - - // Устанавливаем значение обратно - if (currentValue) { - grid_row.doc.value = currentValue; - } -} - -function processAzerbayjanDateField(docValues) { - // Словарь для сопоставления названий месяцев с их номерами - const monthNames = { - "Yanvar": 1, - "Fevral": 2, - "Mart": 3, - "Aprel": 4, - "May": 5, - "İyun": 6, - "İyul": 7, - "Avqust": 8, - "Sentyabr": 9, - "Oktyabr": 10, - "Noyabr": 11, - "Dekabr": 12 - }; - - // Проверяем наличие квартала (rüb) и года (il) - if (docValues && docValues.rüb && docValues.il) { - // Вызываем специальную функцию для обработки квартала - return processQuarterDateField(docValues); - } - - // Проверяем, есть ли у нас значения ay и il - if (docValues && docValues.ay && monthNames[docValues.ay]) { - // Получаем номер месяца - const monthNum = monthNames[docValues.ay]; - - // Получаем год из поля il или используем текущий год - let year = docValues.il || new Date().getFullYear(); - - // Определяем первый день месяца (1-е число) - const firstDay = new Date(year, monthNum - 1, 1); - - // Последний день месяца (получаем последнее число месяца) - const lastDay = new Date(year, monthNum, 0); - - // Форматируем даты в формате YYYY-MM-DD - const firstDayStr = `${year}-${String(monthNum).padStart(2, '0')}-01`; - const lastDayStr = `${year}-${String(monthNum).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`; - - // Возвращаем объект с диапазоном дат - return { - start: firstDayStr, - end: lastDayStr - }; - } - - // Если это не специальное поле или значение не распознано, возвращаем null - return null; -} - -function mapEventToHandler(trigger) { - const eventMap = { - "При открытии документа": "refresh", - "when_document_opens": "refresh", - "document_opens": "refresh" - }; - - return eventMap[trigger] || trigger; -} - -function isPredefinedEvent(trigger) { - const predefinedEvents = [ - "При открытии документа", - "when_document_opens", - "document_opens" - ]; - - return predefinedEvents.includes(trigger); -} - -function addTableRowFormulasSection(wrapper) { - const tableRowSection = $('
'); - tableRowSection.append('

Формулы для строк т.ч.

'); - tableRowSection.append('
'); - wrapper.append(tableRowSection); -} - -function createTableRowFormulaModal() { - if ($("#table_row_formula_modal").length) { - return; - } - - const modalHtml = ` - - `; - - $('body').append(modalHtml); - - // Обработчики для селекторов - $("#table_row_table").on('change', function() { - const selectedTable = $(this).val(); - if (selectedTable) { - loadTableFieldsForSelector(selectedTable); - $("#table_row_field").prop('disabled', false); - } else { - $("#table_row_field").prop('disabled', true).empty().append(''); - } - }); - - // Добавляем обработчик для кнопки "При открытии документа" - $("#table_row_formula_modal").on('click', '.add-predefined-event', function() { - const $triggerContainer = $("#table_row_formula_modal").find(".table-row-trigger"); - // Используем специальную функцию для добавления предопределенного события - addTokenToTriggers("При открытии документа", $triggerContainer, 'event'); - }); - - // Добавляем обработчик для кнопки "Выбрать поле" - $("#table_row_formula_modal").on('click', '.open-trigger-editor-in-row', function() { - const $triggerContainer = $("#table_row_formula_modal").find(".table-row-trigger"); - const frm = cur_frm; - - // Открываем стандартный редактор триггеров, используя контейнер из модального окна строки таблицы - openTriggerFieldsEditorForTableRow(frm, $triggerContainer); - }); -} - -function loadTablesList(frm) { - const $tableSelect = $("#table_row_table"); - $tableSelect.empty().append(''); - - // Получаем все табличные части DocType - frappe.model.with_doctype(frm.doc.target_doctype, function() { - const meta = frappe.get_meta(frm.doc.target_doctype); - if (!meta) return; - - const tableDoctypes = {}; - - meta.fields.forEach(field => { - if (field.fieldtype === 'Table' && field.options) { - $tableSelect.append(``); - tableDoctypes[field.fieldname] = field.options; - } - }); - - // Сохраняем информацию о DocType для табличных частей - $("#table_row_formula_modal").data("table-doctypes", tableDoctypes); - }); -} - -function loadTableFields(tableName) { - const $fieldSelect = $("#table_row_field"); - $fieldSelect.empty().append(''); - - const tableDoctypes = $("#table_row_formula_modal").data("table-doctypes") || {}; - const doctype = tableDoctypes[tableName]; - - if (!doctype) return; - - // Получаем поля DocType табличной части - frappe.model.with_doctype(doctype, function() { - const meta = frappe.get_meta(doctype); - if (!meta) return; - - // Фильтруем только числовые поля для формул - const numericTypes = ['Int', 'Float', 'Currency']; - meta.fields.forEach(field => { - if (field.fieldname && numericTypes.includes(field.fieldtype)) { - $fieldSelect.append(``); - } - }); - }); -} - -function openTableRowFormulaModal(frm, formulaId = null) { - const $modal = $("#table_row_formula_modal"); - const $resultContainer = $modal.find(".table-row-formula"); - const $triggerContainer = $modal.find(".table-row-trigger"); - - // Очистка предыдущих обработчиков и данных - $modal.off('click', '.field-btn'); - $modal.off('click', '.universal-field-btn'); - $modal.off('click', '.operator-btn'); - $modal.off('click', '.save-table-row-formula'); - $modal.off('click', '.add-predefined-event'); - $modal.off('click', '.open-trigger-editor-in-row'); - $modal.off('click', '.select-table-row-trigger-field'); - - $resultContainer.empty(); - $triggerContainer.empty(); - $modal.find(".field-groups-container").empty(); - - // ИСПРАВЛЕНИЕ: явно очищаем ID формулы, если это новая формула - if (!formulaId) { - $modal.removeData("formula-id"); - } - - // Показываем модальное окно с блокировкой контента - $modal.find(".modal-content").append(''); - $modal.modal('show'); - - // Используем обещания для последовательной загрузки данных - new Promise((resolve) => { - // Шаг 1: Получаем информацию о DocType таблиц - const table_doctypes = {}; - getTableDoctypesAsync(frm.doc.target_doctype, table_doctypes, () => { - // Сохраняем результат в глобальных данных модального окна - $modal.data("table_doctypes", table_doctypes); - resolve(); - }); - }) - .then(() => { - return new Promise((resolve) => { - // Шаг 2: Загружаем список табличных частей - loadTablesListAsync(frm, $modal, () => { - resolve(); - }); - }); - }) - .then(() => { - // Шаг 3: Если редактируем существующую формулу, загружаем ее данные - if (formulaId) { - return new Promise((resolve) => { - const tableRowFormulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); - const formula = tableRowFormulas[formulaId]; - - if (formula) { - // Заполняем селекты - $("#table_row_table").val(formula.table_name); - - // Загружаем поля выбранной таблицы - loadTableFieldsAsync(formula.table_name, $modal, () => { - $("#table_row_field").val(formula.field_name); - $("#table_row_index").val(formula.row_index); - - // Загружаем порядок выполнения (если существует, иначе значение по умолчанию 10) - $("#table_row_order").val(formula.order || 10); - - // Заполняем формулу и триггеры - parseAndRenderFormula($resultContainer, formula.formula, true); - parseAndRenderTriggers($triggerContainer, formula.triggers); - - $modal.data("formula-id", formulaId); - resolve(); - }); - } else { - resolve(); - } - }); - } - }) - .then(() => { - // Шаг 4: Загружаем поля для формулы - return new Promise((resolve) => { - populateTableRowFormulaFieldsAsync(frm, $modal, () => { - resolve(); - }); - }); - }) - .then(() => { - // Шаг 5: Настраиваем обработчики событий - - // Обработчики для операторов - $modal.find('.operator-btn').off('click').on('click', function() { - const operator = $(this).data('operator'); - addTokenToFormula(operator, 'operator', $resultContainer); - }); - - // Обработчик для кнопки сохранения - $modal.find('.save-table-row-formula').off('click').on('click', function() { - const tableName = $("#table_row_table").val(); - const fieldName = $("#table_row_field").val(); - const rowIndex = $("#table_row_index").val(); - - if (!tableName || !fieldName || !rowIndex) { - frappe.msgprint({ - title: __('Внимание'), - message: __('Выберите табличную часть, поле и укажите номер строки'), - indicator: 'orange' - }); - return; - } - - const formula = getFormulaText($resultContainer); - const triggers = getTriggerFieldsText($triggerContainer); - - // Сохраняем формулу со всеми необходимыми данными, включая порядок - saveTableRowFormula(frm, { - table_name: tableName, - field_name: fieldName, - row_index: rowIndex, - formula: formula, - triggers: triggers - }, $modal.data("formula-id")); - - $modal.modal('hide'); - }); - - // Обработчик для кнопки "При открытии документа" - $modal.find('.add-predefined-event').off('click').on('click', function() { - addTokenToTriggers("При открытии документа", $triggerContainer, 'event'); - }); - - // Обработчик для кнопки "Выбрать поле" - $modal.find('.open-trigger-editor-in-row').off('click').on('click', function() { - openTriggerFieldsEditorForTableRow(frm, $triggerContainer); - }); - - // Обработчик для кнопки выбора поля строки табличной части - $modal.find('.select-table-row-trigger-field').off('click').on('click', function() { - openTableRowFieldSelector(frm, $triggerContainer, true); - }); - - // Обработчики для кнопок полей (используем делегирование событий) - $modal.off('click', '.field-btn, .universal-field-btn').on('click', '.field-btn, .universal-field-btn', function() { - const fieldText = $(this).text().trim(); - const fieldType = $(this).hasClass('universal-field-btn') ? 'universal-field' : 'field'; - - // Добавляем в формулу - const $token = $(`${fieldText}×`); - $resultContainer.append($token).append(' '); - - // Добавляем обработчики для токенов - addTokenHandlers($resultContainer); - }); - - // Настраиваем перетаскивание и редактирование - initDragAndDrop($modal, $resultContainer, true, frm); - setupEditableContainer($resultContainer); - - // Настраиваем drag and drop для триггеров - initTriggerDragAndDrop($modal, $triggerContainer); - setupTriggerEditableContainer($triggerContainer); - - // ИСПРАВЛЕНИЕ: Добавляем обработчик закрытия модального окна для очистки данных - $modal.off('hidden.bs.modal').on('hidden.bs.modal', function() { - $modal.removeData("formula-id"); - }); - - // Удаляем блокирующий оверлей - $modal.find(".modal-overlay").remove(); - }) - .catch((error) => { - $modal.find(".modal-overlay").remove(); - frappe.msgprint({ - title: __('Ошибка'), - message: __('Не удалось загрузить данные для формулы строки таблицы'), - indicator: 'red' - }); - }); -} - -// Асинхронная функция для получения информации о DocType таблиц с колбэком -function getTableDoctypesAsync(target_doctype, result_object, callback) { - - frappe.call({ - method: "frappe.client.get", - args: { - doctype: "DocType", - name: target_doctype - }, - callback: function(r) { - if (r.message && r.message.fields) { - r.message.fields.forEach(field => { - if (field.fieldtype === 'Table' && field.options) { - result_object[field.fieldname] = field.options; - } - }); - } - - // Если данных нет, используем альтернативный способ через meta - if (Object.keys(result_object).length === 0) { - - const meta = frappe.get_meta(target_doctype); - if (meta && meta.fields) { - meta.fields.forEach(field => { - if (field.fieldtype === 'Table' && field.options) { - result_object[field.fieldname] = field.options; - } - }); - } - } - - // Проверяем результат - if (Object.keys(result_object).length === 0) { - } else { - } - - if (callback) callback(); - } - }); -} - -// Асинхронная функция для загрузки списка таблиц с колбэком -function loadTablesListAsync(frm, $modal, callback) { - const $tableSelect = $("#table_row_table"); - $tableSelect.empty().append(''); - - // Получаем все табличные части DocType - frappe.model.with_doctype(frm.doc.target_doctype, function() { - const meta = frappe.get_meta(frm.doc.target_doctype); - if (!meta) { - if (callback) callback(); - return; - } - - const table_doctypes = $modal.data("table_doctypes") || {}; - - meta.fields.forEach(field => { - if (field.fieldtype === 'Table' && field.options) { - $tableSelect.append(``); - table_doctypes[field.fieldname] = field.options; - } - }); - - // Обновляем обработчик для выбора таблицы - $("#table_row_table").off('change').on('change', function() { - const selectedTable = $(this).val(); - if (selectedTable) { - loadTableFieldsAsync(selectedTable, $modal, null); - $("#table_row_field").prop('disabled', false); - } else { - $("#table_row_field").prop('disabled', true).empty().append(''); - } - }); - - if (callback) callback(); - }); -} - -// Асинхронная функция для загрузки полей таблицы с колбэком -function loadTableFieldsAsync(tableName, $modal, callback) { - const $fieldSelect = $("#table_row_field"); - $fieldSelect.empty().append(''); - - if (!tableName) { - if (callback) callback(); - return; - } - - const table_doctypes = $modal.data("table_doctypes") || {}; - const doctype = table_doctypes[tableName]; - - if (!doctype) { - if (callback) callback(); - return; - } - - // Получаем поля DocType табличной части - frappe.model.with_doctype(doctype, function() { - const meta = frappe.get_meta(doctype); - if (!meta) { - if (callback) callback(); - return; - } - - // Добавляем поля в селект - meta.fields.forEach(field => { - if (field.fieldname && ['Int', 'Float', 'Currency'].includes(field.fieldtype)) { - $fieldSelect.append(``); - } - }); - - // Загружаем поля строки для визуального редактора - populateRowFieldsAsync(tableName, $modal, doctype); - - if (callback) callback(); - }); -} - -// Асинхронная функция для загрузки полей в модальное окно формулы строки таблицы с колбэком -// Асинхронная функция для загрузки полей в модальное окно формулы строки таблицы с колбэком -function populateTableRowFormulaFieldsAsync(frm, $modal, callback) { - const $container = $modal.find(".field-groups-container"); - $container.empty(); - - // Получаем контейнер для формулы - const $formulaContainer = $modal.find(".table-row-formula"); - - // 1. Добавляем раздел с полями основного документа - const $documentFieldsGroup = $(` -
-
Поля документа
-
-
- `); - - const $documentFieldsContainer = $documentFieldsGroup.find('.field-buttons-container'); - - // Получаем все поля основного документа - frappe.model.with_doctype(frm.doc.target_doctype, function() { - const meta = frappe.get_meta(frm.doc.target_doctype); - if (meta && meta.fields) { - meta.fields.forEach(field => { - if (field.fieldname && ['Data', 'Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)) { - const $fieldBtn = $(` - - `); - - $documentFieldsContainer.append($fieldBtn); - } - }); - } - - $container.append($documentFieldsGroup); - - // 2. Добавляем раздел с полями текущей строки таблицы - const $rowFieldsGroup = $(` -
-
Поля текущей строки
-
-
- `); - - $container.append($rowFieldsGroup); - - // 3. Добавляем раздел с универсальными полями - if (frm.doc.universal_fields_storage) { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - - if (Object.keys(universalFields).length > 0) { - const $universalFieldGroup = $(` -
-
Универсальные поля
-
-
- `); - - const $universalButtonsContainer = $universalFieldGroup.find('.field-buttons-container'); - - Object.keys(universalFields).forEach(fieldId => { - const field = universalFields[fieldId]; - - if (!field || !field.name) return; - - const $fieldBtn = $(` - - `); - - $universalButtonsContainer.append($fieldBtn); - }); - - $container.append($universalFieldGroup); - } - } - - // 4. Добавляем раздел с полями отчетов - ДОБАВЛЯЕМ ЭТОТ БЛОК - if (frm.doc.report_fields_storage) { - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - - if (Object.keys(reportFields).length > 0) { - const $reportFieldGroup = $(` -
-
Поля отчетов
-
-
- `); - - const $reportButtonsContainer = $reportFieldGroup.find('.field-buttons-container'); - - Object.keys(reportFields).forEach(fieldId => { - const field = reportFields[fieldId]; - - if (!field || !field.name) return; - - const $fieldBtn = $(` - - `); - - $reportButtonsContainer.append($fieldBtn); - }); - - $container.append($reportFieldGroup); - } - } - - // 5. Добавляем обработчики для кнопок полей - $modal.off('click', '.field-btn, .universal-field-btn, .report-field-btn').on('click', '.field-btn, .universal-field-btn, .report-field-btn', function() { - const fieldText = $(this).text().trim(); - const fieldType = $(this).hasClass('universal-field-btn') ? 'universal-field' : - $(this).hasClass('report-field-btn') ? 'report-field' : 'field'; - - // Добавляем в формулу - const $token = $(`${fieldText}×`); - $formulaContainer.append($token).append(' '); - - // Добавляем обработчики к токену - addTokenHandlers($formulaContainer); - }); - - if (callback) callback(); - }); -} - -// Асинхронная функция для загрузки полей строки таблицы с колбэком -function populateRowFieldsAsync(tableName, $modal, doctype) { - const $rowFieldsContainer = $modal.find('.row-fields-container'); - $rowFieldsContainer.empty(); - - if (!tableName || !doctype) return; - - frappe.model.with_doctype(doctype, function() { - const childMeta = frappe.get_meta(doctype); - if (childMeta && childMeta.fields) { - childMeta.fields.forEach(field => { - if (field.fieldname && ['Data', 'Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)) { - const $fieldBtn = $(` - - `); - - $rowFieldsContainer.append($fieldBtn); - } - }); - } - }); -} - -// Функция для загрузки полей в модальное окно формулы строки таблицы -function populateTableRowFormulaFields($modal, frm) { - const $container = $modal.find(".field-groups-container"); - $container.empty(); - - // Получаем контейнер для формулы - const $formulaContainer = $modal.find(".table-row-formula"); - - // 1. Добавляем раздел с полями основного документа - const $documentFieldsGroup = $(` -
-
Поля документа
-
-
- `); - - const $documentFieldsContainer = $documentFieldsGroup.find('.field-buttons-container'); - - // Получаем все поля основного документа - frappe.model.with_doctype(frm.doc.target_doctype, function() { - const meta = frappe.get_meta(frm.doc.target_doctype); - if (meta && meta.fields) { - meta.fields.forEach(field => { - if (field.fieldname && ['Data', 'Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)) { - const $fieldBtn = $(` - - `); - - $documentFieldsContainer.append($fieldBtn); - } - }); - } - }); - - $container.append($documentFieldsGroup); - - // 2. Добавляем раздел с полями текущей строки таблицы - const $rowFieldsGroup = $(` -
-
Поля текущей строки
-
-
- `); - - $container.append($rowFieldsGroup); - - // Обработчик для выбора таблицы, чтобы показать поля строки - $("#table_row_table").on('change', function() { - const selectedTable = $(this).val(); - if (!selectedTable) return; - - const $rowFieldsContainer = $rowFieldsGroup.find('.field-buttons-container'); - $rowFieldsContainer.empty(); - - // Получаем DocType для выбранной таблицы - const table_doctypes = {}; - getTableDoctypes(frm.doc.target_doctype, table_doctypes); - const childDoctype = table_doctypes[selectedTable]; - - if (childDoctype) { - frappe.model.with_doctype(childDoctype, function() { - const childMeta = frappe.get_meta(childDoctype); - if (childMeta && childMeta.fields) { - childMeta.fields.forEach(field => { - if (field.fieldname && ['Data', 'Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)) { - const $fieldBtn = $(` - - `); - - $rowFieldsContainer.append($fieldBtn); - } - }); - } - }); - } - }); - - // 3. Добавляем раздел с универсальными полями - if (frm.doc.universal_fields_storage) { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - - if (Object.keys(universalFields).length > 0) { - const $universalFieldGroup = $(` -
-
Универсальные поля
-
-
- `); - - const $universalButtonsContainer = $universalFieldGroup.find('.field-buttons-container'); - - Object.keys(universalFields).forEach(fieldId => { - const field = universalFields[fieldId]; - - if (!field || !field.name) return; - - const $fieldBtn = $(` - - `); - - $universalButtonsContainer.append($fieldBtn); - }); - - $container.append($universalFieldGroup); - } - } - - // 4. Добавляем обработчики для кнопок полей - $modal.off('click', '.field-btn, .universal-field-btn').on('click', '.field-btn, .universal-field-btn', function() { - const fieldText = $(this).text().trim(); - const fieldType = $(this).hasClass('universal-field-btn') ? 'universal-field' : 'field'; - - // Добавляем в формулу - const $token = $(`${fieldText}×`); - $formulaContainer.append($token).append(' '); - - // Добавляем обработчики к токену - addTokenHandlers($formulaContainer); - }); -} - -function saveTableRowFormula(frm, formulaData, formulaId = null) { - const newFormulaId = formulaId || frappe.utils.get_random(10); - - try { - let tableRowFormulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); - - // Получаем значение порядка из интерфейса - const order = parseInt($("#table_row_order").val()) || 10; - - // Добавляем порядок в данные формулы - formulaData.order = order; - - // Сохраняем новую или обновленную формулу - tableRowFormulas[newFormulaId] = formulaData; - - // Обновляем значение в форме - frm.set_value("table_row_formulas_storage", JSON.stringify(tableRowFormulas)); - - frappe.show_alert({ - message: __('Формула для строки табличной части сохранена'), - indicator: 'green' - }); - - // Обновляем список формул - renderTableRowFormulas(frm); - } catch (error) { - frappe.msgprint({ - title: __('Ошибка'), - message: __('Не удалось сохранить формулу для строки табличной части.'), - indicator: 'red' - }); - } -} - -function renderTableRowFormulas(frm) { - const $container = $(frm.wrapper).find('.table-row-formulas-list'); - $container.empty(); - - try { - const tableRowFormulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); - - if (Object.keys(tableRowFormulas).length === 0) { - $container.append('
Нет формул для строк табличных частей. Нажмите "Добавить формулу" для создания.
'); - return; - } - - // Создаем таблицу для списка формул - const $table = $(` - - - - - - - - - - - - - -
Табличная частьПолеСтрокаПорядокФормулаОбновлять при измененииДействия
- `); - - const $tbody = $table.find('tbody'); - - // Добавляем формулы в таблицу - Object.keys(tableRowFormulas).forEach(formulaId => { - const formula = tableRowFormulas[formulaId]; - - const $row = $(` - - ${formula.table_name} - ${formula.field_name} - ${formula.row_index} - ${formula.order || 10} - ${formula.formula} - ${formula.triggers || '-'} - - - - - - `); - - $tbody.append($row); - }); - - $container.append($table); - } catch (error) { - $container.append('
Ошибка при загрузке формул для строк табличных частей.
'); - } -} - -function deleteTableRowFormula(frm, formulaId) { - frappe.confirm( - 'Вы уверены, что хотите удалить эту формулу для строки табличной части?', - function() { - try { - let tableRowFormulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); - delete tableRowFormulas[formulaId]; - frm.set_value("table_row_formulas_storage", JSON.stringify(tableRowFormulas)); - - // Перерисовываем список формул - renderTableRowFormulas(frm); - - frappe.show_alert({ - message: __('Формула для строки табличной части удалена'), - indicator: 'green' - }); - } catch (error) { - frappe.msgprint({ - title: __('Ошибка'), - message: __('Не удалось удалить формулу для строки табличной части.'), - indicator: 'red' - }); - } - } - ); -} - -function initTableRowFormulas(frm) { - // Если хранилище формул не существует, создаем его - if (!frm.doc.table_row_formulas_storage) { - frm.set_value("table_row_formulas_storage", "{}"); - } - - // Создаем модальное окно, если его еще нет - createTableRowFormulaModal(); - - // Удаляем существующие обработчики - $(frm.wrapper).off('click', '.add-table-row-formula'); - $(frm.wrapper).off('click', '.edit-table-row-formula'); - $(frm.wrapper).off('click', '.delete-table-row-formula'); - - // Обработчик для кнопки добавления новой формулы - $(frm.wrapper).on('click', '.add-table-row-formula', function() { - openTableRowFormulaModal(frm); - }); - - // Обработчик для кнопки редактирования формулы - $(frm.wrapper).on('click', '.edit-table-row-formula', function() { - const formulaId = $(this).data('formula-id'); - openTableRowFormulaModal(frm, formulaId); - }); - - // Обработчик для кнопки удаления формулы - $(frm.wrapper).on('click', '.delete-table-row-formula', function() { - const formulaId = $(this).data('formula-id'); - deleteTableRowFormula(frm, formulaId); - }); - - // Отображаем существующие формулы - renderTableRowFormulas(frm); -} - -function populateTableRowTriggerFields($modal, frm) { - const $container = $modal.find(".trigger-field-groups-container"); - $container.empty(); - - // Добавляем секцию предопределенных событий - const $predefinedEventsGroup = $(` -
-
Предопределенные события
-
-
- `); - - const $predefinedButtonsContainer = $predefinedEventsGroup.find('.trigger-field-buttons-container'); - - // Добавляем кнопки для предопределенных событий - const predefinedEvents = [ - { name: "При открытии документа", id: "При открытии документа" } - ]; - - predefinedEvents.forEach(event => { - const $eventBtn = $(` - - `); - - $predefinedButtonsContainer.append($eventBtn); - }); - - // Добавляем группу предопределенных событий в начало контейнера - $container.prepend($predefinedEventsGroup); - - // Добавляем секцию универсальных полей, если они есть - if (frm.doc.universal_fields_storage) { - const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); - - if (Object.keys(universalFields).length > 0) { - const $universalFieldGroup = $(` -
-
Универсальные поля
-
-
- `); - - const $universalButtonsContainer = $universalFieldGroup.find('.trigger-field-buttons-container'); - - Object.keys(universalFields).forEach(fieldId => { - const field = universalFields[fieldId]; - - if (!field || !field.name) return; - - const $fieldBtn = $(` - - `); - - $universalButtonsContainer.append($fieldBtn); - }); - - $container.append($universalFieldGroup); - } - } - - // Добавляем обычные поля - if (frm.grouped_trigger_fields) { - Object.keys(frm.grouped_trigger_fields).forEach(group => { - // Пропускаем пустые группы - if (!frm.grouped_trigger_fields[group] || frm.grouped_trigger_fields[group].length === 0) return; - - const $fieldGroup = $(` -
-
${group}
-
-
- `); - - const $buttonsContainer = $fieldGroup.find('.trigger-field-buttons-container'); - - frm.grouped_trigger_fields[group].forEach(field => { - // Создаем кнопку поля с информацией о типе поля - const isTableField = group !== "Основные поля"; - const btnClass = isTableField ? "trigger-field-btn table-field-btn" : "trigger-field-btn"; - - const $fieldBtn = $(` - - `); - - $buttonsContainer.append($fieldBtn); - }); - - $container.append($fieldGroup); - }); - } - - // Добавляем группу для полей строк табличных частей - const $tableRowGroup = $(` -
-
Поля строк табличных частей
-
- -
-
- `); - - $container.append($tableRowGroup); - - // Добавляем обработчик для кнопки выбора поля строки табличной части - $tableRowGroup.find('.select-table-row-trigger-field').on('click', function() { - // Получаем контейнер для триггеров - const $triggerContainer = $modal.find(".trigger-result"); - - // Открываем селектор для поля строки таблицы - openTableRowFieldSelector(frm, $triggerContainer, true); - }); - - // Добавляем стили для выделения различных типов триггеров - if (!$("#trigger-field-styles").length) { - $("head").append(` - - `); - } -} - -function addTableRowFieldsStyles() { - if ($('#table-row-fields-styles').length) return; - - const styles = ` - `; - - $('head').append(styles); -} - -function createTableRowFieldSelectorModal() { - if ($("#table_row_field_selector_modal").length) { - return; - } - - const modalHtml = ` - - `; - - $('body').append(modalHtml); - - // Обработчики для селекторов - $("#row_field_table").on('change', function() { - const selectedTable = $(this).val(); - if (selectedTable) { - loadTableFieldsForSelector(selectedTable); - $("#row_field_name").prop('disabled', false); - } else { - $("#row_field_name").prop('disabled', true).empty().append(''); - } - }); -} - -function loadTableFieldsForSelector(tableName) { - const $fieldSelect = $("#row_field_name"); - $fieldSelect.empty().append(''); - - const tableDoctypes = $("#table_row_field_selector_modal").data("table-doctypes") || {}; - const doctype = tableDoctypes[tableName]; - - if (!doctype) return; - - // Получаем поля DocType табличной части - frappe.model.with_doctype(doctype, function() { - const meta = frappe.get_meta(doctype); - if (!meta) return; - - // Добавляем все поля, которые могут быть использованы в формулах и триггерах - const validTypes = ['Int', 'Float', 'Currency', 'Data', 'Text', 'Date', 'Datetime', 'Check', 'Link', 'Select']; - meta.fields.forEach(field => { - if (field.fieldname && validTypes.includes(field.fieldtype)) { - $fieldSelect.append(``); - } - }); - }); -} - -function loadTablesListForSelector(frm) { - const $tableSelect = $("#row_field_table"); - $tableSelect.empty().append(''); - - // Получаем все табличные части DocType - frappe.model.with_doctype(frm.doc.target_doctype, function() { - const meta = frappe.get_meta(frm.doc.target_doctype); - if (!meta) return; - - const tableDoctypes = {}; - - meta.fields.forEach(field => { - if (field.fieldtype === 'Table' && field.options) { - $tableSelect.append(``); - tableDoctypes[field.fieldname] = field.options; - } - }); - - // Сохраняем информацию о DocType для табличных частей - $("#table_row_field_selector_modal").data("table-doctypes", tableDoctypes); - }); -} - -function openTableRowFieldSelector(frm, targetContainer, isForTrigger = false) { - const $modal = $("#table_row_field_selector_modal"); - - // Сбрасываем форму - $("#row_field_table").val(''); - $("#row_field_name").empty().append('').prop('disabled', true); - $("#row_field_index").val(1); - - // Удаляем старые обработчики - $modal.find('.add-table-row-field').off('click'); - - // Загружаем список табличных частей - loadTablesListForSelector(frm); - - // Сохраняем информацию о целевом контейнере и типе (формула или триггер) - $modal.data("target-container", targetContainer); - $modal.data("is-trigger", isForTrigger); - $modal.data("frm", frm); - - // Обработчик для кнопки добавления - $modal.find('.add-table-row-field').on('click', function() { - const tableName = $("#row_field_table").val(); - const fieldName = $("#row_field_name").val(); - const rowIndex = $("#row_field_index").val(); - - if (!tableName || !fieldName || !rowIndex) { - frappe.msgprint({ - title: __('Внимание'), - message: __('Выберите табличную часть, поле и укажите номер строки'), - indicator: 'orange' - }); - return; - } - - const fieldId = `${tableName}_row_${rowIndex}_${fieldName}`; - const $container = $($modal.data("target-container")); - const isForTrigger = $modal.data("is-trigger"); - - if (isForTrigger) { - // Добавляем поле как триггер - addTokenToTriggers(fieldId, $container, 'table-row'); - } else { - // Добавляем поле в формулу - addTokenToFormula(fieldId, 'field', $container); - } - - $modal.modal('hide'); - }); - - // Показываем модальное окно - $modal.modal('show'); -} - -function generateTableRowFormulasHandlers(table_row_formulas, functions, target_doctype, universal_fields) { - // Создаем функцию для инициализации формул строк табличных частей - let initFunction = ` - // Функция для инициализации формул строк табличных частей - function initialize_table_row_formulas(frm) { - try { - // Получаем информацию о формулах для строк табличных частей - const formulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); - - // ВАЖНО: Преобразуем объект в массив и сортируем его по порядку выполнения - const sortedFormulas = Object.entries(formulas).map(([id, formula]) => { - return { id, ...formula }; - }).sort((a, b) => { - // Сортировка по значению поля order (если порядок не задан, используем 10 по умолчанию) - const orderA = a.order !== undefined ? parseInt(a.order) : 10; - const orderB = b.order !== undefined ? parseInt(b.order) : 10; - return orderA - orderB; - }); - - // Группируем формулы по табличным частям и строкам - const formulasByTableAndRow = {}; - - sortedFormulas.forEach(formula => { - const tableName = formula.table_name; - const rowIndex = parseInt(formula.row_index); - - if (!formulasByTableAndRow[tableName]) { - formulasByTableAndRow[tableName] = {}; - } - - if (!formulasByTableAndRow[tableName][rowIndex]) { - formulasByTableAndRow[tableName][rowIndex] = []; - } - - formulasByTableAndRow[tableName][rowIndex].push(formula); - }); - - // Устанавливаем обработчики для каждой табличной части - Object.keys(formulasByTableAndRow).forEach(tableName => { - if (!frm.doc[tableName]) { - return; - } - - // Для каждой строки таблицы - frm.doc[tableName].forEach((row, rowIdx) => { - // Получаем отсортированные формулы для текущей строки (строки нумеруются с 1, а индексы с 0) - const rowFormulas = formulasByTableAndRow[tableName][rowIdx + 1] || []; - - // Выполняем формулы в порядке сортировки (по полю order) - for (const formula of rowFormulas) { - try { - // Определяем необходимость асинхронного вызова - const usesUniversal = formula.formula && formula.formula.includes("univ("); - const usesReports = formula.formula && formula.formula.includes("report("); - - // Вызываем расчет формулы согласно установленному порядку - if (usesUniversal || usesReports) { - // Для асинхронных вызовов используем await в асинхронной IIFE - (async () => { - try { - await calculate_table_row_field(frm, tableName, rowIdx, formula.field_name, formula.formula, "refresh"); - } catch (asyncError) { - } - })(); - } else { - // Для синхронных вызовов выполняем напрямую - calculate_table_row_field(frm, tableName, rowIdx, formula.field_name, formula.formula, "refresh"); - } - } catch (error) { - } - } - }); - }); - } catch (error) { - } - }`; - - functions.push(initFunction); - - // Создаем функцию для расчета значения поля строки таблицы - let calculateFunction = ` -// Функция для расчета значения поля в строке табличной части -async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, formula, triggerField) { - try { - - if (!frm.doc[tableName] || !frm.doc[tableName][rowIndex]) { - return; - } - - const row = frm.doc[tableName][rowIndex]; - const cdt = frm.fields_dict[tableName].grid.doctype; - const cdn = row.name; - - // === Обработка report() === - let processedFormula = formula; - if (formula.includes("report(")) { - // Регулярное выражение для поиска вызовов report() - const reportRegex = /report\\(([^)]+)\\)/g; - let match; - const reportPromises = []; - const reportPlaceholders = []; - - // Находим все вызовы report() и заменяем их на плейсхолдеры - while ((match = reportRegex.exec(formula)) !== null) { - const fullMatch = match[0]; // report(fieldName) - const fieldName = match[1]; // fieldName - - // Создаем уникальный плейсхолдер - const placeholder = \`__REPORT_PLACEHOLDER_\${reportPromises.length}__\`; - - // Ищем данные отчета в хранилище или используем предопределенный отчет - let reportName, reportFilters, reportField, reportAggregation; - - if (fieldName === "testt") { - reportName = "General Ledger"; - reportFilters = {"company":"$DefaultCompany","from_date":"$AyIlSpecial","to_date":"$AyIlSpecial"}; - reportField = "debit"; - reportAggregation = "sum"; - } else { - // Ищем в хранилище полей отчетов - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - const reportObj = Object.values(reportFields).find(f => f.name === fieldName); - - if (reportObj) { - reportName = reportObj.report; - reportFilters = JSON.parse(reportObj.filters_json || "{}"); - reportField = reportObj.field_name; - reportAggregation = reportObj.aggregation || 'sum'; - } else { - processedFormula = processedFormula.replace(fullMatch, "0"); - continue; - } - } - - // Добавляем промис запроса к отчету - reportPromises.push( - getReportFieldValue( - frm, - reportName, - reportFilters, - reportField, - reportAggregation - ) - ); - - // Запоминаем соответствие плейсхолдера и вызова - reportPlaceholders.push({ - placeholder: placeholder, - original: fullMatch - }); - - // Заменяем вызов отчета на плейсхолдер - processedFormula = processedFormula.replace(fullMatch, placeholder); - } - - // Если нашли вызовы отчетов, ждем выполнения всех запросов - if (reportPromises.length > 0) { - const reportResults = await Promise.all(reportPromises); - - // Заменяем плейсхолдеры на результаты - for (let i = 0; i < reportPlaceholders.length; i++) { - // Убедимся, что результат - числовое значение - const reportValue = reportResults[i] !== undefined ? Number(reportResults[i]) : 0; - processedFormula = processedFormula.replace( - reportPlaceholders[i].placeholder, - reportValue - ); - } - } - } - - // === Обработка row() === - // Заменяем все вызовы row() на доступ к полям текущей строки - let finalFormula = processedFormula; - const rowRegex = /row\\(([^)]+)\\)/g; - finalFormula = finalFormula.replace(rowRegex, (match, fieldName) => { - return \`(parseFloat(row["\${fieldName}"]) || 0)\`; - }); - - // === Обработка val() === - // Заменяем все вызовы val() на доступ к полям документа - const valRegex = /val\\(([^)]+)\\)/g; - finalFormula = finalFormula.replace(valRegex, (match, fieldName) => { - return \`(parseFloat(frm.doc["\${fieldName}"]) || 0)\`; - }); - - // Вычисление выражения с доступом к объекту row - let result = 0; - try { - // Создаем функцию с доступом к контексту row и frm - const evalFunc = new Function('row', 'frm', \`return \${finalFormula};\`); - result = evalFunc(row, frm); - } catch (e) { - } - - if (isNaN(result)) { - result = 0; - } - - // Устанавливаем вычисленное значение - frappe.model.set_value(cdt, cdn, fieldName, result); - - // Обновляем таблицу - frm.refresh_field(tableName); - } catch (err) { - } -}`; - - - functions.push(calculateFunction); - - // Функция для получения индекса строки таблицы - let getRowIndexFunction = ` -// Функция для получения индекса строки по её имени -function get_row_index(frm, tableName, rowName) { - - // Проверяем наличие таблицы - if (!frm.doc[tableName]) { - return -1; - } - - // Ищем индекс строки по имени - for (let i = 0; i < frm.doc[tableName].length; i++) { - if (frm.doc[tableName][i].name === rowName) { - return i; - } - } - return -1; -}`; - - functions.push(getRowIndexFunction); - - return functions; -} - -// Функция для добавления обработчиков событий табличных частей -function addTableRowFormulaHandlers(main_script, table_row_formulas, table_doctypes) { - // Группируем формулы по таблицам и полям-триггерам - const handlers = {}; - - // Собираем все обработчики для триггеров - let triggerHandlers = {}; - - // Специальное хранилище для триггеров в основном документе - const mainDocumentTriggers = {}; - - Object.values(table_row_formulas).forEach(formula => { - const tableName = formula.table_name; - const rowIndex = parseInt(formula.row_index); - const usesUniversal = formula.formula && formula.formula.includes("univ("); - - if (!handlers[tableName]) { - handlers[tableName] = { - childDoctype: table_doctypes[tableName] || `Item`, - triggers: {} - }; - } - - // Если есть триггеры, добавляем их - if (formula.triggers) { - const triggers = formula.triggers.split(',').map(t => t.trim()); - - triggers.forEach(trigger => { - if (!trigger || trigger === "При открытии документа") return; - - // Проверяем, если триггер уже существует в основном документе - if (trigger && !trigger.includes('_')) { - // Это может быть поле основного документа - if (!mainDocumentTriggers[trigger]) { - mainDocumentTriggers[trigger] = []; - } - - mainDocumentTriggers[trigger].push({ - tableName: tableName, - rowIndex: rowIndex, - fieldName: formula.field_name, - formula: formula.formula, - usesUniversal: usesUniversal - }); - } else { - // Это обычный триггер табличной части - if (!handlers[tableName].triggers[trigger]) { - handlers[tableName].triggers[trigger] = []; - } - - handlers[tableName].triggers[trigger].push({ - rowIndex: rowIndex, - fieldName: formula.field_name, - formula: formula.formula, - usesUniversal: usesUniversal - }); - } - }); - } - }); - - // Добавляем обработчики для полей основного документа в объект triggerHandlers - Object.keys(mainDocumentTriggers).forEach(trigger => { - const formulas = mainDocumentTriggers[trigger]; - const hasUniversalField = formulas.some(formula => formula.usesUniversal); - - if (!triggerHandlers[trigger]) { - triggerHandlers[trigger] = { - isAsync: hasUniversalField, - handlers: [] - }; - } else if (hasUniversalField) { - triggerHandlers[trigger].isAsync = true; - } - - // Для каждой формулы, добавляем код для вычисления - formulas.forEach(formula => { - const handlerCode = ` - // Проверяем строку ${formula.rowIndex} в таблице ${formula.tableName} - if (frm.doc.${formula.tableName} && frm.doc.${formula.tableName}.length >= ${formula.rowIndex}) { - try { - // Индекс строки на 1 меньше номера строки - const rowIndex = ${formula.rowIndex - 1}; - const formula = "${formula.formula}"; - - calculate_table_row_field(frm, "${formula.tableName}", rowIndex, "${formula.fieldName}", formula, "${trigger}"); - } catch (error) { - } - }`; - - triggerHandlers[trigger].handlers.push(handlerCode); - }); - }); - - // Теперь создаем обработчики для каждого триггера - Object.keys(triggerHandlers).forEach(trigger => { - const handler = triggerHandlers[trigger]; - - main_script += ` - "${trigger}": ${handler.isAsync ? 'async ' : ''}function(frm) { - ${handler.isAsync ? 'await calculate_universal_fields(frm);' : '// Универсальные поля не используются'} - - ${handler.handlers.join('\n')} - },`; - }); - - // Добавляем обработчики для событий add_row и remove_row для каждой таблицы - Object.keys(handlers).forEach(tableName => { - main_script += ` - "${tableName}_add": function(frm, cdt, cdn) { - // Обновляем индексы строк после добавления новой строки - setTimeout(() => initialize_table_row_formulas(frm), 100); - }, - - "${tableName}_remove": function(frm, cdt, cdn) { - // Обновляем индексы строк после удаления строки - setTimeout(() => initialize_table_row_formulas(frm), 100); - }, -`; - }); - - return main_script; -} - -// Функция для получения индекса строки в табличной части -function generateTableRowAccessFunction() { - return ` -// Функция для получения индекса строки по её имени (name/cdn) -function get_row_index(frm, tableName, rowName) { - - // Проверяем наличие таблицы - if (!frm.doc[tableName]) { - return -1; - } - - // Выводим все строки для отладки - frm.doc[tableName].forEach((row, idx) => { - }); - - // Ищем индекс строки по имени - for (let i = 0; i < frm.doc[tableName].length; i++) { - if (frm.doc[tableName][i].name === rowName) { - return i; - } - } - return -1; -} - -// Функция для доступа к полям конкретных строк таблицы -function table_row(tableName, rowIndex, fieldName) { - // Проверяем наличие необходимых аргументов - if (!tableName || rowIndex === undefined || !fieldName) { - return 0; - } - - // Преобразуем rowIndex в число (на случай, если передана строка) - const rowIdx = parseInt(rowIndex) - 1; // Индексация с 1 для пользователя, но с 0 в массиве - - try { - // Проверяем наличие таблицы и строки - if (!frm.doc[tableName] || !frm.doc[tableName][rowIdx]) { - return 0; - } - - // Получаем значение поля - const value = frm.doc[tableName][rowIdx][fieldName]; - - // Проверяем на NaN и возвращаем значение или 0 - const numValue = parseFloat(value); - if (isNaN(numValue)) { - return 0; - } - return numValue; - } catch (error) { - return 0; - } -} - -// Функция для обновления значения поля конкретной строки таблицы -function set_table_row(frm, tableName, rowIndex, fieldName, value) { - // Проверяем наличие необходимых аргументов - if (!frm || !tableName || rowIndex === undefined || !fieldName) { - return false; - } - - // Преобразуем rowIndex в число (на случай, если передана строка) - const rowIdx = parseInt(rowIndex) - 1; // Индексация с 1 для пользователя, но с 0 в массиве - - try { - // Проверяем наличие таблицы и строки - if (!frm.doc[tableName] || !frm.doc[tableName][rowIdx]) { - return false; - } - - // Получаем строку таблицы - const row = frm.doc[tableName][rowIdx]; - - // Получаем сетевое имя cdt и cdn для Frappe - const cdt = frm.fields_dict[tableName].grid.doctype; - const cdn = row.name; - - // Проверяем, является ли значение NaN, и если да, заменяем его на 0 - let finalValue = value; - if (typeof value === 'number' && isNaN(value)) { - finalValue = 0; - } - - // Обновляем значение поля - frappe.model.set_value(cdt, cdn, fieldName, finalValue); - - return true; - } catch (error) { - return false; - } -} - -// Функция для поиска строки таблицы по значению поля -function find_table_row(frm, tableName, fieldName, value) { - // Проверяем наличие необходимых аргументов - if (!frm || !tableName || !fieldName) { - return -1; - } - - try { - // Проверяем наличие таблицы - if (!frm.doc[tableName]) { - return -1; - } - - // Ищем строку с указанным значением поля - for (let i = 0; i < frm.doc[tableName].length; i++) { - if (frm.doc[tableName][i][fieldName] == value) { - // Возвращаем индекс строки (начиная с 1 для пользователя) - return i + 1; - } - } - - // Если строка не найдена - return -1; - } catch (error) { - return -1; - } -}`; -} - -function processFormulasForTriggersInClientScript(formula_data, table_row_formulas, universal_fields, functions) { - // Получаем все предопределенные события - let refreshHandlers = []; - - // Проходим по всем формулам и собираем информацию о триггерах - Object.keys(formula_data).forEach(fieldname => { - let field_info = formula_data[fieldname]; - if (!field_info || !field_info.triggers) return; - - 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: field_info.formula && field_info.formula.includes("univ("), - uses_reports: field_info.formula && field_info.formula.includes("report("), - is_table_field: field_info.is_table_field - }); - } - }); - - // Проверяем, есть ли формулы строк таблиц с предопределенным событием - Object.keys(table_row_formulas).forEach(formulaId => { - const formula = table_row_formulas[formulaId]; - if (!formula || !formula.triggers) return; - - const triggers = formula.triggers.split(',').map(t => t.trim()); - - // Проверяем наличие предопределенных событий - const hasRefreshTrigger = triggers.some(trigger => isPredefinedEvent(trigger)); - - if (hasRefreshTrigger) { - // Добавляем информацию о формуле строки таблицы - refreshHandlers.push({ - type: 'table_row', - table_name: formula.table_name, - row_index: formula.row_index, - field_name: formula.field_name, - formula: formula.formula, - uses_universal: formula.formula && formula.formula.includes("univ("), - uses_reports: formula.formula && formula.formula.includes("report(") - }); - } - }); - - // Обработка универсальных полей в триггерах - let fieldsUsingUniversal = []; - - // Проходим по всем триггерам и ищем использование универсальных полей - Object.keys(formula_data).forEach(fieldname => { - let field_info = formula_data[fieldname]; - if (!field_info || !field_info.triggers) return; - - let triggers = field_info.triggers.split(',').map(t => t.trim()); - - // Проверяем наличие универсальных полей в триггерах - const hasUniversalTrigger = triggers.some(trigger => trigger.startsWith('univ(')); - - if (hasUniversalTrigger) { - // Добавляем в список полей с универсальными триггерами - fieldsUsingUniversal.push({ - field: fieldname, - uses_universal_in_formula: field_info.formula && field_info.formula.includes("univ("), - is_table_field: field_info.is_table_field - }); - } - }); - - // Формируем функцию для обработки предопределенных событий и возвращаем её - if (refreshHandlers.length > 0 || fieldsUsingUniversal.length > 0) { - // Генерируем функцию обработки предопределенных событий и возвращаем её - let eventHandlerFunction = ` -// Функция для обработки предопределенных событий -async function handle_predefined_events(frm) { - try { - // Сначала вычисляем универсальные поля, если они используются - ${refreshHandlers.some(h => h.uses_universal) || fieldsUsingUniversal.some(f => f.uses_universal_in_formula) ? - 'await calculate_universal_fields(frm);' : - '// Универсальные поля не используются'} - - // Обработка обычных полей с предопределенным событием - ${refreshHandlers.filter(h => !h.is_table_field && h.type !== 'table_row').map(handler => { - if (handler.uses_universal || handler.uses_reports) { - return ` - // Расчет поля ${handler.field} (использует универсальные поля или отчеты) - await calculate_${handler.field}(frm);`; - } else { - return ` - // Расчет поля ${handler.field} - calculate_${handler.field}(frm);`; - } - }).join('')} - - // Обработка строк табличных частей с предопределенным событием - ${refreshHandlers.filter(h => h.type === 'table_row').map(handler => { - // Проверяем, требуется ли асинхронный вызов - const needsAsync = handler.uses_universal || handler.uses_reports; - return ` - // Расчет поля ${handler.field_name} в строке ${handler.row_index} таблицы ${handler.table_name} - try { - ${needsAsync ? 'await ' : ''}calculate_table_row_field(frm, "${handler.table_name}", ${parseInt(handler.row_index) - 1}, "${handler.field_name}", "${handler.formula}", "refresh"); - } catch (error) { - }`; - }).join('')} - - // Обработка полей, использующих универсальные поля в триггерах - ${fieldsUsingUniversal.map(field => { - // Для полей, которые используют универсальные поля в формуле, вызов уже был выше - if (field.uses_universal_in_formula) return ''; - - if (field.is_table_field) { - const parts = field.field.split('_'); - const table_name = parts[0]; - const field_name = parts.slice(1).join('_'); - - return ` - // Обновление поля ${field_name} в таблице ${table_name} (триггер от универсального поля) - update_table_fields(frm, "${table_name}", "${field_name}");`; - } else { - return ` - // Расчет поля ${field.field} (триггер от универсального поля) - calculate_${field.field}(frm);`; - } - }).join('')} - } catch (error) { - } -}`; - - return eventHandlerFunction; - } - - return null; -} - -function generateEventHandlersFunction(refreshHandlers, fieldsUsingUniversal, universal_fields) { - // Создаем обработчик предопределенных событий - let eventHandlerFunction = ` -// Функция для обработки предопределенных событий -async function handle_predefined_events(frm) { - try { - // Сначала вычисляем универсальные поля, если они используются - ${refreshHandlers.some(h => h.uses_universal) || fieldsUsingUniversal.some(f => f.uses_universal_in_formula) ? - 'await calculate_universal_fields(frm);' : - '// Универсальные поля не используются'} - - // Обработка обычных полей с предопределенным событием - ${refreshHandlers.filter(h => h.type !== 'table_row').map(handler => { - if (handler.uses_universal) { - return ` - // Расчет поля ${handler.field} (использует универсальные поля) - await calculate_${handler.field}(frm);`; - } else { - return ` - // Расчет поля ${handler.field} - calculate_${handler.field}(frm);`; - } - }).join('')} - - // Обработка строк табличных частей с предопределенным событием - ${refreshHandlers.filter(h => h.type === 'table_row').map(handler => { - // Проверяем, требуется ли асинхронный вызов - const needsAsync = handler.uses_universal || handler.uses_reports; - return ` - // Расчет поля ${handler.field_name} в строке ${handler.row_index} таблицы ${handler.table_name} - try { - ${needsAsync ? 'await ' : ''}calculate_table_row_field(frm, "${handler.table_name}", ${parseInt(handler.row_index) - 1}, "${handler.field_name}", "${handler.formula}", "refresh"); - } catch (error) { - }`; - }).join('')} - - // Обработка полей, использующих универсальные поля в триггерах - ${fieldsUsingUniversal.map(field => { - // Для полей, которые используют универсальные поля в формуле, вызов уже был выше - if (field.uses_universal_in_formula) return ''; - - if (field.is_table_field) { - const parts = field.field.split('_'); - const table_name = parts[0]; - const field_name = parts.slice(1).join('_'); - - return ` - // Обновление поля ${field_name} в таблице ${table_name} (триггер от универсального поля) - update_table_fields(frm, "${table_name}", "${field_name}");`; - } else { - return ` - // Расчет поля ${field.field} (триггер от универсального поля) - calculate_${field.field}(frm);`; - } - }).join('')} - } catch (error) { - } -}`; - - return eventHandlerFunction; -} - -function addTriggersToClientScript(main_script, formula_data, tableTriggersSet, refreshHandlers, universal_fields) { - // Создаем объект для хранения обработчиков по триггерам - const triggerHandlers = {}; - - // Обрабатываем все формулы для основного документа - Object.keys(formula_data).forEach(fieldname => { - let field_info = formula_data[fieldname]; - - if (!field_info || typeof field_info !== "object" || !field_info.formula || !field_info.triggers) { - return; - } - - // Получаем информацию о поле - let is_table_field = field_info.is_table_field; - let formula = field_info.formula.trim(); - let uses_universal = formula.includes("univ("); - - // Если это не поле табличной части, обрабатываем его триггеры - if (!is_table_field) { - 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: uses_universal - }); - - // Удаляем предопределенное событие из списка триггеров - triggers = triggers.filter(trigger => !isPredefinedEvent(trigger)); - } - - // Обрабатываем универсальные поля в триггерах - const hasUniversalTrigger = triggers.some(trigger => trigger.startsWith('univ(')); - - if (hasUniversalTrigger) { - // Создаем специальный обработчик для универсальных полей в триггерах, если его ещё нет - if (!triggerHandlers["universal_fields_updated"]) { - triggerHandlers["universal_fields_updated"] = { - needsAsync: uses_universal, - handlers: [] - }; - } - - // Обновляем флаг async если текущее поле требует асинхронности - if (uses_universal) { - triggerHandlers["universal_fields_updated"].needsAsync = true; - } - - // Добавляем обработчик в список - triggerHandlers["universal_fields_updated"].handlers.push({ - fieldname: fieldname, - uses_universal: uses_universal - }); - - // Удаляем универсальные поля из списка триггеров - triggers = triggers.filter(trigger => !trigger.startsWith('univ(')); - } - - // Фильтруем триггеры, исключая те, что уже добавлены в table_field_triggers - const filteredTriggers = triggers.filter(trigger => !tableTriggersSet.has(trigger)); - - // Удаляем дубликаты триггеров - const uniqueTriggers = [...new Set(filteredTriggers)]; - - uniqueTriggers.forEach(trigger => { - if (trigger) { - // Определяем, является ли это полем строки таблицы (table_row_...) - const isTableRowField = trigger.includes('_row_'); - - if (isTableRowField) { - // Извлекаем информацию о таблице, строке и поле - const match = trigger.match(/^([^_]+)_row_(\d+)_(.+)$/); - if (match) { - const tableName = match[1]; - const rowIndex = parseInt(match[2]); - const fieldInRow = match[3]; - - // Создаем ключ для таблицы строк - const tableTriggerKey = `${tableName}_on_form_rendered`; - - // Инициализируем, если еще не существует - if (!triggerHandlers[tableTriggerKey]) { - triggerHandlers[tableTriggerKey] = { - needsAsync: false, - handlers: [] - }; - } - - // Обновляем флаг async - if (uses_universal) { - triggerHandlers[tableTriggerKey].needsAsync = true; - } - - // Добавляем обработчик для строки таблицы - triggerHandlers[tableTriggerKey].handlers.push({ - fieldname: fieldname, - uses_universal: uses_universal, - tableName: tableName, - rowIndex: rowIndex, - fieldInRow: fieldInRow - }); - } - } else { - // Для обычного поля, инициализируем обработчик триггера, если его еще нет - if (!triggerHandlers[trigger]) { - triggerHandlers[trigger] = { - needsAsync: uses_universal, - handlers: [] - }; - } - - // Обновляем флаг async - if (uses_universal) { - triggerHandlers[trigger].needsAsync = true; - } - - // Добавляем обработчик - triggerHandlers[trigger].handlers.push({ - fieldname: fieldname, - uses_universal: uses_universal - }); - } - } - }); - } - }); - - // Теперь генерируем JavaScript-код для всех триггеров - Object.keys(triggerHandlers).forEach(trigger => { - const handlerInfo = triggerHandlers[trigger]; - const isAsync = handlerInfo.needsAsync; - - if (trigger === "universal_fields_updated") { - // Специальный обработчик для универсальных полей - main_script += ` "universal_fields_updated": ${isAsync ? 'async ' : ''}function(frm) { - // Пересчитываем поля, которые зависят от универсальных полей -${handlerInfo.handlers.map(handler => { - return ` ${isAsync && handler.uses_universal ? 'await ' : ''}calculate_${handler.fieldname}(frm);`; -}).join('\n')} - },\n`; - } else if (trigger.endsWith('_on_form_rendered')) { - // Специальный обработчик для строк таблиц - const tableName = trigger.replace('_on_form_rendered', ''); - main_script += ` "${trigger}": function(frm) { - // Устанавливаем обработчики для полей в строках таблицы ${tableName} -${handlerInfo.handlers.map(handler => { - return ` try { - if (frm.doc.${handler.tableName} && frm.doc.${handler.tableName}.length >= ${handler.rowIndex}) { - const row = frm.doc.${handler.tableName}[${handler.rowIndex - 1}]; - const cdt = frm.fields_dict.${handler.tableName}.grid.doctype; - const cdn = row.name; - - // Устанавливаем обработчик для поля ${handler.fieldInRow} - if (!frm.__row_handlers) frm.__row_handlers = {}; - if (!frm.__row_handlers['${handler.tableName}_${handler.rowIndex}_${handler.fieldInRow}']) { - frm.__row_handlers['${handler.tableName}_${handler.rowIndex}_${handler.fieldInRow}'] = function(doc, cdt, cdn) { - if (doc.name === cdn) { - ${handler.uses_universal ? 'setTimeout(async function() { await calculate_' + handler.fieldname + '(frm); }, 0);' : 'calculate_' + handler.fieldname + '(frm);'} - } - }; - - // Регистрируем обработчик - frappe.ui.form.on(cdt, { - "${handler.fieldInRow}": frm.__row_handlers['${handler.tableName}_${handler.rowIndex}_${handler.fieldInRow}'] - }); - } - } - } catch (error) { - }`; -}).join('\n')} - },\n`; - } else { - // Обычный обработчик события - main_script += ` "${trigger}": ${isAsync ? 'async ' : ''}function(frm) { -${handlerInfo.handlers.map(handler => { - return ` ${isAsync && handler.uses_universal ? 'await ' : ''}calculate_${handler.fieldname}(frm);`; -}).join('\n')} - },\n`; - } - }); - - return main_script; -} - -function createTriggerToken(fieldname, fieldType) { - // Определяем класс токена в зависимости от типа поля - let tokenClass = "trigger-token field"; - - switch (fieldType) { - case 'event': - tokenClass = "trigger-token field highlight-event"; - break; - case 'universal': - tokenClass = "trigger-token field universal-field"; - break; - case 'table': - tokenClass = "trigger-token field table-field"; - break; - case 'table-row': - tokenClass = "trigger-token field table-row-field"; - break; - default: - tokenClass = "trigger-token field"; - } - - return $(`${fieldname}×`); -} - -function initTriggerTokenEvents($token) { - // Удаление токена - $token.find('.token-remove').on('click', function(e) { - e.stopPropagation(); - $(this).parent().remove(); - }); - - // Перетаскивание токена - $token.attr('draggable', 'true'); - - $token.off('dragstart').on('dragstart', function(e) { - window.draggedTriggerElement = this; - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'trigger-token', - value: $(this).data('value'), - html: $(this)[0].outerHTML, - tokenClass: $(this).attr('class') - })); - - $(this).addClass('dragging'); - }); - - $token.off('dragend').on('dragend', function() { - $(this).removeClass('dragging'); - window.draggedTriggerElement = null; - }); -} - -function addTokenStyles() { - if ($("#token-styles").length) return; - - const tokenStyles = ` - `; - - $('head').append(tokenStyles); -} - -// Обновляем функцию openTriggerFieldsEditorForTableRow -function openTriggerFieldsEditorForTableRow(frm, $triggerContainer) { - const $modal = $("#trigger_fields_modal"); - - // Очистка предыдущих обработчиков и данных - $modal.off('click', '.trigger-field-btn'); - $modal.off('click', '.select-table-row-trigger-field'); - $modal.find('.apply-triggers').off('click'); - $modal.find(".trigger-result").empty(); - $modal.find(".trigger-field-groups-container").empty(); - - // Копируем текущие триггеры из контейнера строки таблицы - const currentTriggers = getTriggerFieldsText($triggerContainer); - - // Заполняем текущие триггеры в модальное окно триггеров - if (currentTriggers) { - parseAndRenderTriggers($modal.find(".trigger-result"), currentTriggers); - } - - // Заполняем панель с полями - populateTriggerFieldsPanel($modal, frm, frm.grouped_trigger_fields || {}); - - // Помечаем, что это для контейнера строки таблицы - $modal.data("is-for-table-row", true); - $modal.data("table-row-container", $triggerContainer); - - // Обработчик для кнопки применения триггеров - $modal.find('.apply-triggers').off('click').on('click', function() { - const triggers = getTriggerFieldsText($modal.find(".trigger-result")); - - // Проверяем, для контейнера строки таблицы или обычного поля - if ($modal.data("is-for-table-row")) { - // Очищаем контейнер строки таблицы - $triggerContainer.empty(); - - // Переносим триггеры из модального окна в контейнер строки таблицы - if (triggers) { - parseAndRenderTriggers($triggerContainer, triggers); - } - } else { - // Стандартное поведение для обычного поля - const targetFieldname = $modal.data("target-field"); - const $targetInput = $(frm.wrapper).find(`.trigger-input[data-fieldname="${targetFieldname}_triggers"]`); - $targetInput.val(triggers); - } - - $modal.modal('hide'); - }); - - // Обработчики для кнопок полей - $modal.off('click', '.trigger-field-btn').on('click', '.trigger-field-btn', function() { - const fieldname = $(this).data('fieldname'); - - // Определяем тип поля по классу кнопки - let fieldType = 'field'; - if ($(this).hasClass('highlight-event')) { - fieldType = 'event'; - } else if ($(this).hasClass('universal-field-btn')) { - fieldType = 'universal'; - } else if ($(this).hasClass('table-field-btn')) { - fieldType = 'table'; - } - - addTokenToTriggers(fieldname, $modal.find(".trigger-result"), fieldType); - }); - - // Обработчик для кнопки выбора поля строки табличной части - $modal.off('click', '.select-table-row-trigger-field').on('click', '.select-table-row-trigger-field', function() { - openTableRowFieldSelector(frm, $modal.find(".trigger-result"), true); - }); - - // Увеличиваем z-index модального окна, чтобы оно было поверх других - $modal.css('z-index', 1100); - - // Настраиваем фон - $('.modal-backdrop').not(':first').css({ - 'z-index': 1050, - 'opacity': 0.7 - }); - - // Показываем модальное окно - $modal.modal('show'); - - // Инициализируем перетаскивание - initTriggerDragAndDrop($modal, $modal.find(".trigger-result")); - - // Настраиваем редактируемый контейнер - setupTriggerEditableContainer($modal.find(".trigger-result")); - - // Фокусируемся на редактируемом поле - $modal.find(".trigger-result").focus(); -} - -function addReportFieldsSection(wrapper) { - const reportSection = $('
'); - reportSection.append('

Поля отчетов

'); - reportSection.append('
'); - wrapper.append(reportSection); -} - -function initReportFields(frm) { - // Если хранилище полей отчетов не существует, создаем его - if (!frm.doc.report_fields_storage) { - frm.set_value("report_fields_storage", "{}"); - } - - // Инициализируем обработчики событий - attachReportFieldsEvents(frm); - - // Отображаем существующие поля отчетов - renderReportFields(frm); -} - -function attachReportFieldsEvents(frm) { - // Сначала удаляем существующие обработчики событий - $(frm.wrapper).off('click', '.add-report-field'); - $(frm.wrapper).off('click', '.edit-report-field'); - $(frm.wrapper).off('click', '.delete-report-field'); - - // Обработчик нажатия на кнопку "Добавить поле" - $(frm.wrapper).on('click', '.add-report-field', function() { - openReportFieldModal(frm); - }); - - // Обработчик для клика по кнопке редактирования - $(frm.wrapper).on('click', '.edit-report-field', function(e) { - e.stopPropagation(); - const fieldId = $(this).data('field-id'); - editReportField(frm, fieldId); - }); - - // Обработчик для клика по кнопке удаления - $(frm.wrapper).on('click', '.delete-report-field', function(e) { - e.stopPropagation(); - const fieldId = $(this).data('field-id'); - deleteReportField(frm, fieldId); - }); -} - -function createReportFieldModal() { - if ($("#report_fields_modal").length) { - return; - } - - const modalHtml = ` - `; - - $('body').append(modalHtml); -} - -function openReportFieldModal(frm, fieldId = null, fieldCopy = null) { - // Создаем/обновляем модальное окно - createReportFieldModal(); - - // Получаем значения для редактирования - let fieldValues = fieldCopy || {}; - if (!fieldCopy && fieldId) { - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - fieldValues = reportFields[fieldId] || {}; - } - - // Создаем диалог с формой для настройки поля отчета - const d = new frappe.ui.Dialog({ - title: fieldId ? 'Редактирование поля отчета' : 'Создание поля отчета', - fields: [ - { - label: 'Название поля', - fieldname: 'name', - fieldtype: 'Data', - reqd: 1, - default: fieldValues.name || '' - }, - { - label: 'Отчет', - fieldname: 'report', - fieldtype: 'Link', - options: 'Report', - reqd: 1, - default: fieldValues.report || '', - filters: { - "report_type": "Script Report" - }, - onchange: function() { - const selectedReport = d.get_value('report'); - - 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(); - } - } - }); - } 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(); - } - } - }, - { - label: 'Фильтры отчета', - fieldname: 'filters_section', - fieldtype: 'Section Break', - hidden: 1 - }, - { - fieldname: 'filters_helper', - fieldtype: 'HTML', - options: '
Настройте фильтры для отчета. Эти фильтры будут использоваться при каждом вычислении формулы.
', - hidden: 1 - }, - { - fieldname: 'filters_container', - fieldtype: 'HTML', - options: '
' - }, - { - fieldname: 'filters_json', - fieldtype: 'Code', - 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 - }, - { - label: 'Поле отчета', - fieldname: 'field_section', - fieldtype: 'Section Break' - }, - { - label: 'Поле отчета', - fieldname: 'field_name', - fieldtype: 'Select', - options: [], - reqd: 1, - default: fieldValues.field_name || '' - }, - { - label: 'Агрегация', - fieldname: 'aggregation', - fieldtype: 'Select', - options: [ - {label: 'Сумма', value: '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({ - title: __('Внимание'), - message: __('Заполните все обязательные поля'), - indicator: 'orange' - }); - return; - } - - // Обновляем значение фильтров - values.filters_json = filters_json; - - // Сохраняем данные - saveReportField(frm, values, fieldId); - - d.hide(); - } - }); - - // Сохраняем информацию о специальных датах - d.has_special_dates = fieldValues.__has_special_dates; - - // Показываем диалог - d.show(); - - // Если редактируем существующее поле, загружаем данные - if (fieldId && fieldValues.report) { - setTimeout(() => { - d.fields_dict.report.df.onchange.call(d.fields_dict.report); - }, 200); - } -} - -// Функция для восстановления специальных значений дат в UI -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]; - - // Если есть контрол для этого поля - 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; // Останавливаем цикл - } - }); - } - } - } - } - }); - } catch (e) { - } -} - -function generateFiltersHtml(filters, currentValues) { - let html = '
'; - - filters.forEach(filter => { - const fieldId = `filter_${filter.fieldname}`; - const currentValue = currentValues[filter.fieldname] || filter.default; - - html += ` -
-
${filter.label || filter.fieldname}
-
-
`; - }); - - html += '
'; - return html; -} - -function initFilterControls(d, currentValues) { - if (!d.report_filters) { - frappe.throw("Отсутствует информация о фильтрах отчета"); - return; - } - - // Инициализируем объект для хранения контролов - 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; - } - - 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; - - // Создаем контрол в зависимости от типа поля - 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; - - 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(); - } - }); - } 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; - } - - // Обработчик изменения для обновления JSON - $container.find('input').on('change', function() { - updateFiltersJson(d); - }); - } - 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); - }; - - // Сохраняем контрол - d.filter_controls[filter.fieldname] = control; - } - }); - - // Обновляем JSON фильтров - 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; - } - - // Показываем какие фильтры мы пытаемся использовать - frappe.show_alert({ - message: `Загружаем поля отчета '${reportName}' с фильтрами: ${JSON.stringify(filters)}`, - indicator: 'blue' - }); - - // Загружаем информацию о фильтрах отчета, если ее еще нет - 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 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')); - } - } else { - // Если полей нет, очищаем селект - d.set_df_property('field_name', 'options', []); - d.fields_dict.field_name.refresh(); - - frappe.throw(`В отчете '${reportName}' не найдено подходящих числовых полей. Пожалуйста, выберите другой отчет.`); - } - } - }); - } -} - -// Функция сохранения поля отчета -function saveReportField(frm, values, fieldId = null) { - // Используем имя поля в качестве идентификатора или указанный ID - const newFieldId = fieldId || frappe.utils.get_random(10); - - try { - let reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - - // Проверка на дублирование имени поля - const existingField = Object.keys(reportFields).find(key => - key !== newFieldId && reportFields[key].name === values.name - ); - - if (existingField) { - frappe.msgprint({ - title: __('Внимание'), - message: __('Поле с таким именем уже существует.'), - indicator: 'orange' - }); - 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, - report: values.report, - filters_json: values.filters_json, - field_name: values.field_name, - aggregation: values.aggregation - }; - - frm.set_value("report_fields_storage", JSON.stringify(reportFields)); - - frappe.show_alert({ - message: __('Поле отчета сохранено'), - indicator: 'green' - }); - - renderReportFields(frm); - } catch (error) { - frappe.msgprint({ - title: __('Ошибка'), - message: __('Не удалось сохранить поле отчета: ') + error.message, - indicator: 'red' - }); - } -} - -function renderReportFields(frm) { - const $container = $(frm.wrapper).find('.report-fields-list'); - $container.empty(); - - try { - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - - if (Object.keys(reportFields).length === 0) { - $container.append('
Нет полей отчетов. Нажмите "Добавить поле отчета" для создания.
'); - return; - } - - Object.keys(reportFields).forEach(fieldId => { - const field = reportFields[fieldId]; - - const $fieldButton = $(` -
- - -
- `); - - // Показываем/скрываем кнопки действий при наведении - $fieldButton.hover( - function() { $(this).find('.field-actions').show(); }, - function() { $(this).find('.field-actions').hide(); } - ); - - $container.append($fieldButton); - }); - - // Инициализируем drag-n-drop для полей отчетов - $container.find('.report-field-btn').each(function() { - $(this).attr('draggable', 'true'); - - $(this).off('dragstart').on('dragstart', function(e) { - const fieldId = $(this).data('field-id'); - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - const field = reportFields[fieldId]; - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'report-field', - fieldId: fieldId, - name: field.name - })); - }); - }); - } catch (error) { - $container.append('
Ошибка при загрузке полей отчетов.
'); - } -} - -function editReportField(frm, fieldId) { - try { - const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - const field = reportFields[fieldId]; - - if (!field) { - frappe.throw("Поле отчета не найдено"); - return; - } - - // Важно: создаем копию поля, чтобы не изменять оригинал в хранилище - const fieldCopy = {...field}; - - // Обрабатываем фильтры перед открытием диалога - if (fieldCopy.filters_json) { - try { - let filters = JSON.parse(fieldCopy.filters_json); - const processedFilters = {}; - - // Проходим по всем фильтрам и обрабатываем специальные значения - Object.keys(filters).forEach(key => { - let value = filters[key]; - - // Обработка дат для месяца (ay) - if (key.toLowerCase().includes('date') && value === "$AyIlSpecial") { - // Заменяем специальное значение на обычную дату для временного использования - // Потом мы восстановим специальное значение при сохранении - processedFilters["__original_" + key] = "$AyIlSpecial"; // Сохраняем оригинальное значение - processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата - } - // Обработка дат для квартала (rüb) - else if (key.toLowerCase().includes('date') && value === "$RubIlSpecial") { - // Аналогично сохраняем оригинальное значение для квартала - processedFilters["__original_" + key] = "$RubIlSpecial"; // Сохраняем оригинальное значение - processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата - } - // Обработка ссылок на поля документа - else if (typeof value === 'string' && value.startsWith("doc.") && - (key.toLowerCase().includes('date') || key.toLowerCase().includes('from') || key.toLowerCase().includes('to'))) { - // Обрабатываем также ссылки на поля документа - processedFilters["__original_" + key] = value; // Сохраняем оригинальное значение - processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата - } - // Обычные значения без обработки - else { - processedFilters[key] = value; - } - }); - - // Обновляем JSON фильтров в копии - fieldCopy.filters_json = JSON.stringify(processedFilters); - fieldCopy.__has_special_dates = true; // Маркер для последующего восстановления - } catch (e) { - } - } - - // Открываем модальное окно с обработанной копией поля - openReportFieldModal(frm, fieldId, fieldCopy); - } catch (error) { - frappe.msgprint({ - title: __('Ошибка'), - message: __('Не удалось открыть поле отчета для редактирования: ') + error.message, - indicator: 'red' - }); - } -} - -function deleteReportField(frm, fieldId) { - frappe.confirm( - 'Вы уверены, что хотите удалить это поле отчета?', - function() { - try { - let reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); - delete reportFields[fieldId]; - frm.set_value("report_fields_storage", JSON.stringify(reportFields)); - - // Перерисовываем список полей - renderReportFields(frm); - - frappe.show_alert({ - message: __('Поле отчета удалено'), - indicator: 'green' - }); - } catch (error) { - frappe.msgprint({ - title: __('Ошибка'), - message: __('Не удалось удалить поле отчета.'), - indicator: 'red' - }); - } - } - ); -} - -// Обработка формул строк таблиц для генерации клиентского скрипта -function processTableRowFormulas(table_row_formulas, table_doctypes) { - // Хранилище для объединенных обработчиков - const triggerHandlers = {}; - - // Проверяем, что объект table_row_formulas не пустой - if (!table_row_formulas || Object.keys(table_row_formulas).length === 0) { - return triggerHandlers; - } - - // Получаем отсортированные формулы по порядку выполнения - const sortedFormulas = Object.entries(table_row_formulas).map(([id, formula]) => { - return { id, ...formula }; - }).sort((a, b) => { - // Сортировка по значению поля order (если порядок не задан, используем 10 по умолчанию) - const orderA = a.order !== undefined ? parseInt(a.order) : 10; - const orderB = b.order !== undefined ? parseInt(b.order) : 10; - return orderA - orderB; - }); - - // Группируем формулы по триггерам - sortedFormulas.forEach(formula => { - const tableName = formula.table_name; - const rowIndex = parseInt(formula.row_index); - const usesUniversal = formula.formula && formula.formula.includes("univ("); - const usesReports = formula.formula && formula.formula.includes("report("); - - // Если есть триггеры, добавляем их - if (formula.triggers) { - const triggers = formula.triggers.split(',').map(t => t.trim()); - - triggers.forEach(trigger => { - // Пропускаем триггер "При открытии документа", он обрабатывается отдельно - if (!trigger || trigger === "При открытии документа" || - trigger === "when_document_opens" || trigger === "document_opens") { - return; - } - - // Инициализируем обработчик триггера, если его еще нет - if (!triggerHandlers[trigger]) { - triggerHandlers[trigger] = { - isAsync: usesUniversal || usesReports, - handlers: [] - }; - } else if (usesUniversal || usesReports) { - // Если текущая формула требует async, помечаем весь обработчик как async - triggerHandlers[trigger].isAsync = true; - } - - // Код для вычисления формулы, с учетом порядка выполнения - const handlerCode = ` - // Формула строки ${rowIndex} в таблице ${tableName} (порядок: ${formula.order || 10}) - if (frm.doc.${tableName} && frm.doc.${tableName}.length >= ${rowIndex}) { - try { - // Индекс строки на 1 меньше номера строки - const rowIndex = ${rowIndex - 1}; - const formula = "${formula.formula}"; - - ${usesUniversal || usesReports ? 'await ' : ''}calculate_table_row_field(frm, "${tableName}", rowIndex, "${formula.field_name}", formula, "${trigger}"); - } catch (error) { - } - }`; - - triggerHandlers[trigger].handlers.push(handlerCode); - }); - } - }); - - // Добавляем обработчики для событий add_row и remove_row для каждой таблицы - const tables = new Set(); - sortedFormulas.forEach(formula => { - tables.add(formula.table_name); - }); - - tables.forEach(tableName => { - // Add handler - triggerHandlers[`${tableName}_add`] = { - isAsync: false, - handlers: [ - `// Обновляем индексы строк после добавления новой строки`, - `setTimeout(() => initialize_table_row_formulas(frm), 100);` - ] - }; - - // Remove handler - triggerHandlers[`${tableName}_remove`] = { - isAsync: false, - handlers: [ - `// Обновляем индексы строк после удаления строки`, - `setTimeout(() => initialize_table_row_formulas(frm), 100);` - ] - }; - }); - - return triggerHandlers; -} - -// Функция для обновления JSON фильтров -function updateFiltersJson(d) { - try { - // Собираем все значения фильтров - const values = {}; - Object.keys(d.filter_controls || {}).forEach(fieldname => { - const ctrl = d.filter_controls[fieldname]; - if (ctrl) { - try { - const value = ctrl.get_value(); - if (value !== undefined && value !== null && value !== '') { - // Сохраняем все значения как есть - values[fieldname] = value; - - // Проверка на специальные значения дат - if ((value === "$AyIlSpecial" || value === "$RubIlSpecial" || - (typeof value === 'string' && value.startsWith("doc."))) && - (fieldname.toLowerCase().includes('date') || - fieldname.toLowerCase().includes('from') || - fieldname.toLowerCase().includes('to'))) { - - // Сохраняем оригинальное значение отдельно - values[`__original_${fieldname}`] = value; - - // Временно заменяем значение на валидную дату для проверок - values[fieldname] = frappe.datetime.get_today(); - - // Отмечаем, что у нас есть специальные даты - d.set_value('has_special_dates', 1); - } - } - } catch (e) { - frappe.show_alert({ - message: `Ошибка при получении значения фильтра ${fieldname}: ${e.message}`, - indicator: 'red' - }); - } - } - }); - - // Обновляем JSON фильтров - if (Object.keys(values).length > 0) { - d.set_value('filters_json', JSON.stringify(values, null, 2)); - } - } catch (e) { - frappe.show_alert({ - message: `Ошибка при обновлении фильтров: ${e.message}`, - indicator: 'red' - }); - } -} - -// Функция для обработки квартала (rüb) и года (il) -function processQuarterDateField(docValues) { - // Проверяем, есть ли у нас значения rüb и il - if (docValues && docValues.rüb && docValues.il) { - // Получаем номер квартала (преобразуем из строки вида "1 Rüb" в число 1) - const quarterStr = docValues.rüb; - const quarterMatch = quarterStr.match(/(\d+)/); - - if (!quarterMatch) { - return null; - } - - const quarterNum = parseInt(quarterMatch[1]); - - // Получаем год из поля il или используем текущий год - let year = docValues.il || new Date().getFullYear(); - - // Определяем начальный и конечный месяц квартала - let startMonth, endMonth; - - switch(quarterNum) { - case 1: - startMonth = 1; // Январь - endMonth = 3; // Март - break; - case 2: - startMonth = 4; // Апрель - endMonth = 6; // Июнь - break; - case 3: - startMonth = 7; // Июль - endMonth = 9; // Сентябрь - break; - case 4: - startMonth = 10; // Октябрь - endMonth = 12; // Декабрь - break; - default: - return null; - } - - // Определяем первый день квартала (1-е число начального месяца) - const firstDay = new Date(year, startMonth - 1, 1); - - // Последний день квартала (последнее число конечного месяца) - const lastDay = new Date(year, endMonth, 0); - - // Форматируем даты в формате YYYY-MM-DD - const firstDayStr = `${year}-${String(startMonth).padStart(2, '0')}-01`; - const lastDayStr = `${year}-${String(endMonth).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`; - - // Возвращаем объект с диапазоном дат - return { - start: firstDayStr, - end: lastDayStr - }; - } - - // Если это не специальное поле или значение не распознано, возвращаем null - return null; -} \ No newline at end of file +}}); diff --git a/formula_editor/formula_editor/doctype/formula_editor/test.js b/formula_editor/formula_editor/doctype/formula_editor/test.js new file mode 100644 index 0000000..e69de29 diff --git a/formula_editor/hooks.py b/formula_editor/hooks.py index 3163c9f..f3abe45 100644 --- a/formula_editor/hooks.py +++ b/formula_editor/hooks.py @@ -24,7 +24,7 @@ app_license = "MIT" # include js in doctype views doctype_js = { - "DocType": "public/js/doctype_formula_editor.js", + "Formula Editor": "formula_editor/doctype/formula_editor/formula_editor_main.js", } # include js in doctype list views diff --git a/formula_editor/public/js/formula_editor_dragdrop.js b/formula_editor/public/js/formula_editor_dragdrop.js new file mode 100644 index 0000000..602c33b --- /dev/null +++ b/formula_editor/public/js/formula_editor_dragdrop.js @@ -0,0 +1,458 @@ +function initDragAndDrop($modal, $resultContainer, isTableField, frm) { + // Очищаем предыдущие обработчики + $modal.find('.field-btn, .operator-btn, .universal-field-btn, .report-field-btn').off('dragstart'); + $resultContainer.off('dragover dragleave drop'); + + // Настраиваем перетаскивание существующих токенов + setupTokenDragging($resultContainer); + + // Настраиваем перетаскивание полей из панели + $modal.find('.field-btn').off('dragstart').on('dragstart', function(e) { + const fieldname = $(this).data('fieldname'); + const label = $(this).data('label'); + const group = $(this).data('group'); + + // Проверяем принадлежность к группе + const isTableFieldGroup = group !== "Основные поля"; + const fieldData = { + type: 'field', + fieldname: fieldname, + label: label, + group: group, + isTableField: isTableFieldGroup + }; + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(fieldData)); + }); + + // Настраиваем перетаскивание универсальных полей + $modal.find('.universal-field-btn').off('dragstart').on('dragstart', function(e) { + const fieldId = $(this).data('field-id'); + const fieldname = $(this).data('fieldname'); + const label = $(this).data('label'); + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'universal-field', + fieldId: fieldId, + fieldname: fieldname, + label: label + })); + }); + + // Настраиваем перетаскивание полей отчетов + $modal.find('.report-field-btn').off('dragstart').on('dragstart', function(e) { + const fieldId = $(this).data('field-id'); + const fieldname = $(this).data('fieldname'); + const label = $(this).data('label'); + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'report-field', + fieldId: fieldId, + fieldname: fieldname, + label: label + })); + }); + + // Настраиваем перетаскивание операторов + $modal.find('.operator-btn').off('dragstart').on('dragstart', function(e) { + const operator = $(this).data('operator'); + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'operator', + value: operator + })); + }); + + // Обработчики для целевого контейнера + $resultContainer.off('dragover').on('dragover', function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).addClass('drag-hover'); + + updateDropMarker(e, $(this)); + }); + + $resultContainer.off('dragleave').on('dragleave', function(e) { + $(this).removeClass('drag-hover'); + $(this).find('.drop-marker').remove(); + }); + + $resultContainer.off('drop').on('drop', function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).removeClass('drag-hover'); + $(this).find('.drop-marker').remove(); + + handleDrop(e, $(this), isTableField, frm); + }); +} + +function initTriggerDragAndDrop($modal, $triggerContainer) { + // Очищаем предыдущие обработчики + $modal.find('.field-btn, .universal-field-btn, .trigger-field-btn').off('dragstart'); + $triggerContainer.off('dragover dragleave drop'); + + // Настраиваем перетаскивание существующих токенов в триггер-контейнере + $triggerContainer.find('.trigger-token').each(function() { + $(this).attr('draggable', 'true'); + + $(this).off('dragstart').on('dragstart', function(e) { + // Сохраняем ссылку на текущий элемент + window.draggedTriggerElement = this; + + // Добавляем данные для переноса + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'trigger-token', + value: $(this).data('value'), + html: $(this)[0].outerHTML, + tokenClass: $(this).attr('class') + })); + + // Добавляем класс перетаскивания + $(this).addClass('dragging'); + }); + + // При завершении перетаскивания + $(this).off('dragend').on('dragend', function() { + $(this).removeClass('dragging'); + window.draggedTriggerElement = null; + }); + }); + + // Настраиваем перетаскивание полей из панели в триггер-контейнер + $modal.find('.field-btn, .universal-field-btn, .trigger-field-btn').off('dragstart').on('dragstart', function(e) { + const fieldname = $(this).data('fieldname'); + const label = $(this).data('label'); + let fieldType = 'field'; + + if ($(this).hasClass('universal-field-btn')) { + fieldType = 'universal'; + } else if ($(this).hasClass('highlight-event')) { + fieldType = 'event'; + } else if ($(this).hasClass('table-field-btn')) { + fieldType = 'table'; + } + + const fieldData = { + type: 'trigger-field', + fieldname: fieldname, + label: label, + fieldType: fieldType + }; + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(fieldData)); + }); + + // Обработка dragover для триггер-контейнера + $triggerContainer.off('dragover').on('dragover', function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).addClass('drag-hover'); + + // Обновляем маркер сброса + updateDropMarker(e, $(this)); + }); + + // Обработка dragleave для триггер-контейнера + $triggerContainer.off('dragleave').on('dragleave', function(e) { + e.preventDefault(); + $(this).removeClass('drag-hover'); + $(this).find('.drop-marker').remove(); + }); + + // Обработка drop для триггер-контейнера + $triggerContainer.off('drop').on('drop', function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).removeClass('drag-hover'); + $(this).find('.drop-marker').remove(); + + // Обрабатываем сброшенные данные + handleTriggerDrop(e, $(this)); + }); +} + +function updateDropMarker(e, $container) { + const x = e.originalEvent.clientX; + const y = e.originalEvent.clientY; + + let position = { element: null, position: 'after' }; + + // Проходим по всем дочерним элементам контейнера + $container.contents().each(function() { + if (this.nodeType === Node.TEXT_NODE) return; + + const rect = this.getBoundingClientRect(); + + // Если курсор находится над элементом + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { + position.element = this; + + // Определяем, до или после элемента + if (x < rect.left + rect.width / 2) { + position.position = 'before'; + } else { + position.position = 'after'; + } + + return false; // Прерываем перебор + } + }); + + $container.find('.drop-marker').remove(); + + if (position.element) { + const $marker = $(''); + + if (position.position === 'before') { + $(position.element).before($marker); + } else { + $(position.element).after($marker); + } + } +} + +function handleDrop(e, $container, isTableField, frm) { + try { + // Получаем данные из события перетаскивания + const dataTransfer = e.originalEvent.dataTransfer.getData('text/plain'); + + // Проверяем, что данные не пустые + if (!dataTransfer) { + return; + } + + const data = JSON.parse(dataTransfer); + + const x = e.originalEvent.clientX; + const y = e.originalEvent.clientY; + + let position = findDropPosition(x, y, $container); + + // Обработка перетаскивания существующих токенов + if (data.type === 'token' && window.draggedElement) { + handleExistingTokenDrop(window.draggedElement, position, $container); + return; + } + + // Создание нового элемента + let $newElement; + + if (data.type === 'universal-field') { + // Получаем универсальные поля + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + const field = universalFields[data.fieldId]; + + if (field) { + const formulaText = `univ(${field.name})`; + $newElement = $(`${formulaText}×`); + } + } else if (data.type === 'report-field') { + // Обработка поля отчета + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + const field = reportFields[data.fieldId]; + + if (field) { + const formulaText = `report(${field.name})`; + $newElement = $(`${formulaText}×`); + } + } else if (data.type === 'field') { + $newElement = createFieldToken(data, isTableField); + } else if (data.type === 'operator') { + $newElement = createOperatorToken(data.value); + } + + // Вставляем новый элемент + if ($newElement) { + insertElementAtPosition($newElement, position, $container); + + // Инициализируем события для нового элемента + initializeTokenEvents($newElement); + } + } catch (error) { + } +} + +function handleTriggerDrop(e, $container) { + try { + const data = JSON.parse(e.originalEvent.dataTransfer.getData('text/plain')); + + // Получаем позицию сброса + const x = e.originalEvent.clientX; + const y = e.originalEvent.clientY; + let position = findDropPosition(x, y, $container); + + // Обработка перетаскивания существующих токенов + if (data.type === 'trigger-token' && window.draggedTriggerElement) { + const $dragged = $(window.draggedTriggerElement); + + if (position.element) { + if (position.position === 'before') { + $(position.element).before($dragged); + $(position.element).before(' '); + } else { + $(position.element).after($dragged); + $(position.element).after(' '); + } + } else { + $container.append($dragged); + $container.append(' '); + } + + window.draggedTriggerElement = null; + return; + } + + // Обработка перетаскивания полей из панели + if (data.type === 'field' || data.type === 'trigger-field') { + // Проверяем, не добавлено ли уже это поле + const fieldname = data.fieldname; + const existingTokens = $container.find(`.trigger-token[data-value="${fieldname}"]`); + if (existingTokens.length > 0) { + return; // Если поле уже добавлено, не добавляем его снова + } + + // Определяем тип поля и создаем соответствующий токен + let fieldType = 'field'; + if (data.fieldType === 'universal') { + fieldType = 'universal'; + } else if (data.fieldType === 'table') { + fieldType = 'table'; + } else if (data.fieldType === 'table-row') { + fieldType = 'table-row'; + } else if (data.fieldType === 'event') { + fieldType = 'event'; + } + + // Создаем токен + const $token = createTriggerToken(fieldname, fieldType); + + // Вставляем токен в нужную позицию + if (position.element) { + if (position.position === 'before') { + $(position.element).before($token); + $(position.element).before(' '); + } else { + $(position.element).after($token); + $(position.element).after(' '); + } + } else { + $container.append($token); + $container.append(' '); + } + + // Инициализируем события для нового токена + initTriggerTokenEvents($token); + } + } catch (error) { + } +} + +function findDropPosition(x, y, $container) { + let position = { element: null, position: 'after' }; + + // Проходим по всем дочерним элементам контейнера + $container.contents().each(function() { + if (this.nodeType === Node.TEXT_NODE) return; + + const rect = this.getBoundingClientRect(); + + // Если курсор находится над элементом + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { + position.element = this; + + // Определяем, до или после элемента + if (x < rect.left + rect.width / 2) { + position.position = 'before'; + } else { + position.position = 'after'; + } + + return false; // Прерываем перебор + } + }); + + return position; +} + +function handleExistingTokenDrop(draggedElement, position, $container) { + const $dragged = $(draggedElement); + + if (position.element) { + if (position.position === 'before') { + $(position.element).before($dragged); + $(position.element).before(' '); + } else { + $(position.element).after($dragged); + $(position.element).after(' '); + } + } else { + $container.append($dragged); + $container.append(' '); + } + + window.draggedElement = null; +} + +function createFieldToken(data, isTableField) { + let formulaText; + + // Определяем тип поля и формулу + const fieldInfo = determineFieldType(data.fieldname, data.group, isTableField); + + if (fieldInfo.type === 'row') { + formulaText = `row(${fieldInfo.fieldName})`; + } else if (fieldInfo.type === 'sum') { + formulaText = `sum(${fieldInfo.fieldName})`; + } else { + formulaText = `val(${fieldInfo.fieldName})`; + } + + return $(`${formulaText}×`); +} + +function createOperatorToken(value) { + return $(`${value}×`); +} + +function insertElementAtPosition($element, position, $container) { + if (position.element) { + if (position.position === 'before') { + $(position.element).before($element); + $(position.element).before(' '); + } else { + $(position.element).after($element); + $(position.element).after(' '); + } + } else { + $container.append($element); + $container.append(' '); + } +} + +function initializeTokenEvents($token) { + $token.find('.token-remove').on('click', function(e) { + e.stopPropagation(); + $(this).parent().remove(); + }); + + $token.attr('draggable', 'true'); + + $token.off('dragstart').on('dragstart', function(e) { + window.draggedElement = this; + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'token', + value: $(this).data('value'), + html: $(this)[0].outerHTML + })); + + $(this).addClass('dragging'); + return true; + }); + + $token.off('dragend').on('dragend', function(e) { + $(this).removeClass('dragging'); + window.draggedElement = null; + }); +} \ No newline at end of file diff --git a/formula_editor/public/js/formula_editor_formulas.js b/formula_editor/public/js/formula_editor_formulas.js new file mode 100644 index 0000000..0fb4cc3 --- /dev/null +++ b/formula_editor/public/js/formula_editor_formulas.js @@ -0,0 +1,477 @@ +function getFormulaText($container) { + // Собираем все токены и текст в строку + let formula = ''; + + $container.contents().each(function() { + if (this.nodeType === Node.TEXT_NODE) { + formula += this.textContent; + } else { + const $elem = $(this); + if ($elem.hasClass('formula-token')) { + formula += $elem.data('value'); + } + } + }); + + // Удаляем лишние пробелы + return formula.replace(/\s+/g, ' ').trim(); +} + +function parseAndRenderFormula($container, formula, isTableField) { + if (!formula) return; + + // Регулярные выражения для поиска функций и операторов + const funcRegex = /(val|row|sum|univ|report|table)\(([^)]+)\)/g; + const operatorsRegex = /([+\-*/()%])/g; + + // Сохраняем текущую позицию + let lastIndex = 0; + let match; + let formulaParts = []; + + // Поиск всех функций + while ((match = funcRegex.exec(formula)) !== null) { + // Добавляем текст до функции + if (match.index > lastIndex) { + formulaParts.push({ + type: 'text', + value: formula.substring(lastIndex, match.index) + }); + } + + // Добавляем функцию + formulaParts.push({ + type: 'function', + value: match[0], + func: match[1], + param: match[2] + }); + + lastIndex = match.index + match[0].length; + } + + // Добавляем оставшийся текст после последней функции + if (lastIndex < formula.length) { + formulaParts.push({ + type: 'text', + value: formula.substring(lastIndex) + }); + } + + // Разбираем операторы в частях, которые не являются функциями + let finalParts = []; + formulaParts.forEach(part => { + if (part.type === 'function') { + finalParts.push(part); + } else if (part.type === 'text') { + let text = part.value; + lastIndex = 0; + + // Сбрасываем lastIndex для operatorsRegex + operatorsRegex.lastIndex = 0; + + // Разбираем операторы + while ((match = operatorsRegex.exec(text)) !== null) { + // Добавляем текст до оператора + if (match.index > lastIndex) { + const textBefore = text.substring(lastIndex, match.index).trim(); + if (textBefore) { + finalParts.push({ + type: 'text', + value: textBefore + }); + } + } + + // Добавляем оператор + finalParts.push({ + type: 'operator', + value: match[1] + }); + + lastIndex = match.index + match[1].length; + } + + // Добавляем оставшийся текст после последнего оператора + if (lastIndex < text.length) { + const textAfter = text.substring(lastIndex).trim(); + if (textAfter) { + finalParts.push({ + type: 'text', + value: textAfter + }); + } + } + } + }); + + // Рендерим части формулы + finalParts.forEach(part => { + if (part.type === 'function') { + // Определяем класс в зависимости от типа функции + let tokenClass = 'field'; + if (part.func === 'univ') { + tokenClass = 'field universal-field'; + } else if (part.func === 'report') { + tokenClass = 'field report-field'; + } else if (part.func === 'table') { + tokenClass = 'field table-field'; + } + + const $token = $(`${part.value}×`); + $container.append($token).append(' '); + } else if (part.type === 'operator') { + const $token = $(`${part.value}×`); + $container.append($token).append(' '); + } else if (part.type === 'text') { + $container.append(document.createTextNode(`${part.value} `)); + } + }); + + // Добавляем обработчики для токенов + addTokenHandlers($container); + + return formula; +} + +function addTokenHandlers($container) { + // Удаление токенов + $container.find('.token-remove').off('click').on('click', function(e) { + e.stopPropagation(); + $(this).parent().remove(); + }); + + // Перетаскивание токенов + $container.find('.formula-token').each(function() { + $(this).attr('draggable', 'true'); + + $(this).off('dragstart').on('dragstart', function(e) { + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'token', + value: $(this).data('value'), + html: $(this)[0].outerHTML + })); + window.draggedElement = this; + }); + + $(this).off('dragend').on('dragend', function() { + window.draggedElement = null; + }); + }); +} + +function addTokenToFormula(token, type, $resultContainer, isTableField = false, group = null, frm = null) { + if (type === 'field') { + let formulaText; + + // Определяем тип поля и формулу + const fieldInfo = determineFieldType(token, group, isTableField); + + if (fieldInfo.type === 'table_row') { + // Для полей конкретных строк таблиц + formulaText = `val(${fieldInfo.fullFieldName})`; + } else if (fieldInfo.type === 'row') { + formulaText = `row(${fieldInfo.fieldName})`; + } else if (fieldInfo.type === 'sum') { + let tableName = fieldInfo.tableName || token.match(/^([a-z_]+)/)[1]; + let fieldWithoutPrefix = token.substring(tableName.length); + if (fieldWithoutPrefix.startsWith('_')) { + fieldWithoutPrefix = fieldWithoutPrefix.substring(1); + } + + formulaText = `sum(${tableName}_${fieldWithoutPrefix})`; + } else { + formulaText = `val(${fieldInfo.fieldName})`; + } + + const $token = $(`${formulaText}×`); + $resultContainer.append($token).append(' '); + } else if (type === 'operator') { + const $token = $(`${token}×`); + $resultContainer.append($token).append(' '); + } else if (type === 'universal-field') { + // Обработка для универсальных полей + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + const field = universalFields[token]; + if (field) { + const formulaText = `univ(${field.name})`; + const $token = $(`${formulaText}×`); + $resultContainer.append($token).append(' '); + } + } else if (type === 'report-field') { + // Обработка для полей отчетов + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + const field = reportFields[token]; + if (field) { + const formulaText = `report(${field.name})`; + const $token = $(`${formulaText}×`); + $resultContainer.append($token).append(' '); + } + } + + // Переносим курсор в конец контейнера + setEndOfContentEditable($resultContainer[0]); + + // Добавляем обработчики для токенов + addTokenHandlers($resultContainer); +} + +function determineFieldType(fieldName, group, isInTableField) { + // Проверяем, является ли поле ссылкой на строку табличной части + const rowPattern = /^([a-z_]+)_row_(\d+)_([a-z_]+)$/i; + const rowMatch = fieldName.match(rowPattern); + + if (rowMatch) { + const tableName = rowMatch[1]; + const rowIndex = rowMatch[2]; + const fieldNameInRow = rowMatch[3]; + + return { + type: 'table_row', + tableName: tableName, + rowIndex: rowIndex, + fieldName: fieldNameInRow, + fullFieldName: fieldName + }; + } + + // Получаем части имени поля + // Проверяем существование имен групп "items", "taxes" и т.д. без подчеркивания + const tablePrefixes = ["items", "taxes", "packed_items", "payments", "advances", "timesheets", "payment_schedule", "sales_team"]; + + // Проверяем, начинается ли fieldName с известного префикса таблицы + let isTableField = false; + let tableName = ""; + + for (const prefix of tablePrefixes) { + if (fieldName.startsWith(prefix)) { + // Это поле табличной части без подчеркивания (например, "itemsqty") + isTableField = true; + tableName = prefix; + break; + } + } + + // Если не нашли по префиксу, проверяем по первой части с подчеркиванием + if (!isTableField) { + const parts = fieldName.split('_'); + if (parts.length > 1) { + const potentialTableName = parts[0]; + if (tablePrefixes.includes(potentialTableName)) { + isTableField = true; + tableName = potentialTableName; + } + } + } + + // Определяем тип формулы + if (isTableField) { + // Для полей таблицы + if (isInTableField) { + // В табличной части используем row() + // Извлекаем имя поля без префикса таблицы + let fieldNameWithoutTable = fieldName; + if (fieldName.startsWith(tableName)) { + fieldNameWithoutTable = fieldName.substring(tableName.length); + // Если есть подчеркивание, удаляем его + if (fieldNameWithoutTable.startsWith('_')) { + fieldNameWithoutTable = fieldNameWithoutTable.substring(1); + } + } + + return { + type: 'row', + fieldName: fieldNameWithoutTable + }; + } else { + // В основном документе используем sum() + return { + type: 'sum', + fieldName: fieldName, + tableName: tableName + }; + } + } else { + // Для обычных полей всегда val() + return { + type: 'val', + fieldName: fieldName + }; + } +} + +function openFormulaBuilder(frm, targetFieldname, isTableField, currentFormula, fields, grouped_fields) { + const $modal = $("#formula_builder_modal"); + const $resultContainer = $modal.find(".formula-result"); + + // Очистка предыдущих обработчиков и данных + $modal.off('click', '.field-btn'); + $modal.off('click', '.universal-field-btn'); + $modal.off('click', '.report-field-btn'); + $modal.off('click', '.operator-btn'); + $modal.find('.apply-formula').off('click'); + $resultContainer.empty(); + $modal.find(".field-groups-container").empty(); + + // Заполняем текущую формулу, если она есть + if (currentFormula) { + parseAndRenderFormula($resultContainer, currentFormula, isTableField); + } + + // Заполняем панель с полями + populateFieldsPanel($modal, frm, grouped_fields, isTableField); + + // Сохраняем данные о текущем поле + $modal.data("target-field", targetFieldname); + $modal.data("is-table-field", isTableField); + + // Обработчики для кнопок операторов + $modal.find('.operator-btn').off('click').on('click', function() { + const operator = $(this).data('operator'); + addTokenToFormula(operator, 'operator', $resultContainer); + }); + + // Обработчик для кнопки применения формулы + $modal.find('.apply-formula').off('click').on('click', function() { + const formula = getFormulaText($resultContainer); + const $targetInput = $(frm.wrapper).find(`.formula-input[data-fieldname="${targetFieldname}"]`); + $targetInput.val(formula); + $modal.modal('hide'); + }); + + // Обработчики для кнопок полей - используем делегирование событий + $modal.off('click', '.field-btn').on('click', '.field-btn', function() { + const fieldname = $(this).data('fieldname'); + addTokenToFormula(fieldname, 'field', $resultContainer, isTableField, $(this).data('group')); + }); + + // Обработчики для кнопок универсальных полей + $modal.off('click', '.universal-field-btn').on('click', '.universal-field-btn', function() { + const fieldId = $(this).data('field-id'); + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + const field = universalFields[fieldId]; + + if (field) { + const formulaText = `univ(${field.name})`; + const $token = $(`${formulaText}×`); + $resultContainer.append($token).append(' '); + + // Добавляем обработчики для токенов + addTokenHandlers($resultContainer); + + // Переносим курсор в конец контейнера + setEndOfContentEditable($resultContainer[0]); + } + }); + + // Обработчики для кнопок полей отчетов + $modal.off('click', '.report-field-btn').on('click', '.report-field-btn', function() { + const fieldId = $(this).data('field-id'); + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + const field = reportFields[fieldId]; + + if (field) { + const formulaText = `report(${field.name})`; + const $token = $(`${formulaText}×`); + $resultContainer.append($token).append(' '); + + // Добавляем обработчики для токенов + addTokenHandlers($resultContainer); + + // Переносим курсор в конец контейнера + setEndOfContentEditable($resultContainer[0]); + } + }); + + // Показываем модальное окно + $modal.modal('show'); + + // Инициализируем перетаскивание + initDragAndDrop($modal, $resultContainer, isTableField, frm); + + // Настраиваем редактируемый контейнер + setupEditableContainer($resultContainer); + + // Фокусируемся на редактируемом поле + $resultContainer.focus(); +} + +function setupEditableContainer($container) { + // Обработчик нажатия клавиш в контейнере формулы + $container.off('keydown').on('keydown', function(e) { + // Если нажата клавиша Delete или Backspace + if (e.keyCode === 8 || e.keyCode === 46) { + const selection = window.getSelection(); + + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + + if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) { + // Текстовый узел - обычное удаление + return true; + } else { + // Возможно, выделен токен + const $selectedNode = $(range.commonAncestorContainer); + + if ($selectedNode.hasClass('formula-token') || $selectedNode.parents('.formula-token').length) { + // Удаляем токен + const $token = $selectedNode.hasClass('formula-token') ? + $selectedNode : $selectedNode.parents('.formula-token'); + $token.remove(); + e.preventDefault(); + return false; + } + } + } + } + + return true; + }); + + // Обработчик вставки текста + $container.off('paste').on('paste', function(e) { + e.preventDefault(); + + // Получаем вставляемый текст + let text = ''; + if (e.clipboardData || e.originalEvent.clipboardData) { + text = (e.clipboardData || e.originalEvent.clipboardData).getData('text/plain'); + } else if (window.clipboardData) { + text = window.clipboardData.getData('Text'); + } + + // Вставляем текст как обычный текст + if (text) { + document.execCommand('insertText', false, text); + } + }); +} + +function setupTokenDragging($container) { + $container.find('.formula-token').each(function() { + $(this).attr('draggable', 'true'); + + $(this).off('dragstart').on('dragstart', function(e) { + // Сохраняем ссылку на текущий элемент + window.draggedElement = this; + + // Добавляем данные для переноса + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'token', + value: $(this).data('value'), + html: $(this)[0].outerHTML + })); + + // Добавляем класс перетаскивания + $(this).addClass('dragging'); + + return true; + }); + + // При завершении перетаскивания + $(this).off('dragend').on('dragend', function(e) { + $(this).removeClass('dragging'); + window.draggedElement = null; + }); + }); +} \ 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 new file mode 100644 index 0000000..41144e7 --- /dev/null +++ b/formula_editor/public/js/formula_editor_generator.js @@ -0,0 +1,460 @@ +function update_or_create_client_script(target_doctype, script_code) { + // Имя для нашего клиентского скрипта формул + const formula_script_name = `Formula_Editor_Script_for_${target_doctype}`; + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Client Script", + filters: { dt: target_doctype }, + fields: ["name", "script"] + }, + callback: function(r) { + if (r.message && r.message.length > 0) { + // Находим наш скрипт по имени или по содержимому + let formula_script = r.message.find(script => + script.name === formula_script_name || + (script.script && script.script.includes('// Generated by Formula Editor')) + ); + + if (formula_script) { + + frappe.call({ + method: "frappe.client.set_value", + args: { + doctype: "Client Script", + name: formula_script.name, + fieldname: { + script: script_code, + // Обновляем имя, если оно было другим + name: formula_script_name + } + }, + callback: function(response) { + if (response.message) { + } else { + } + } + }); + } else { + createNewFormulaScript(target_doctype, script_code, formula_script_name); + } + } else { + createNewFormulaScript(target_doctype, script_code, formula_script_name); + } + } + }); +} + +function createNewFormulaScript(target_doctype, script_code, script_name) { + // Добавляем комментарий для идентификации скрипта в будущем + const marked_script_code = `// Generated by Formula Editor\n${script_code}`; + + frappe.call({ + method: "frappe.client.insert", + args: { + doc: { + doctype: "Client Script", + name: script_name, + dt: target_doctype, + script_type: "Client", + script: marked_script_code, + enabled: 1 + } + }, + callback: function(response) { + if (response.message) { + } else { + } + } + }); +} + +function delete_client_script_for_doctype(doctype) { + if (!doctype) { + return; + } + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Client Script", + filters: { dt: doctype }, + fields: ["name"] + }, + callback: function(r) { + if (r.message && r.message.length > 0) { + const script_names = r.message.map(script => script.name); + + // Удаляем каждый найденный скрипт + script_names.forEach(script_name => { + frappe.call({ + method: "frappe.client.delete", + args: { + doctype: "Client Script", + name: script_name + }, + callback: function(response) { + if (response.exc) { + } else { + } + } + }); + }); + } else { + } + } + }); +} + +function generateTransformExpressionFunction(universal_fields, report_fields) { + return ` + function transform_expression(expression, for_child_table = false, table_name = null) { + let transformed = expression; + let needsAsync = false; + let reportCalls = []; + + // === Обработка report(...) === + const reportRegex = /report\\(([^)]+)\\)/g; + let match; + + // Находим все вызовы report() в выражении + while ((match = reportRegex.exec(expression)) !== null) { + const reportName = match[1]; // Имя поля отчета + const fullMatch = match[0]; // Полный текст match - "report(xyz)" + needsAsync = true; + + // Создаем уникальный плейсхолдер + const placeholder = \`__REPORT_\${reportCalls.length}__\`; + + // Ищем соответствующее поле отчета по имени + ${Object.keys(report_fields).map(fieldId => { + const field = report_fields[fieldId]; + if (!field || !field.name) return ''; + + let filtersJson = "{}"; + try { + const filtersObj = JSON.parse(field.filters_json || "{}"); + + if (filtersObj.company && typeof filtersObj.company === 'string' && + !filtersObj.company.startsWith('$')) { + filtersObj.company = "$DefaultCompany"; + } + + Object.keys(filtersObj).forEach(key => { + if (key.toLowerCase().includes('date') || + key.toLowerCase().includes('from') || + key.toLowerCase().includes('to')) { + + const value = filtersObj[key]; + + if (typeof value === 'string' && + (value === "$AyIlSpecial" || value === "$RubIlSpecial" || + value.startsWith("doc."))) { + // Сохраняем оригинальное значение + } + else if (typeof value === 'string') { + filtersObj[key] = "$AyIlSpecial"; + } + } + }); + + filtersJson = JSON.stringify(filtersObj); + } catch (e) { + filtersJson = "{}"; + } + + return ` + if (reportName === "${field.name}") { + reportCalls.push({ + placeholder: placeholder, + reportName: "${field.report}", + filters: JSON.parse(${JSON.stringify(filtersJson)}), + fieldName: "${field.field_name}", + aggregation: "${field.aggregation || 'sum'}" + }); + + transformed = transformed.replace(fullMatch, placeholder); + }`; + }).join('')} + } + + // === ИСПРАВЛЕНО: Обработка table(...) === + const tableRegex = /table\\(([^,)]+),\\s*([^,)]+),\\s*([^)]+)\\)/g; + let tableMatch; + while ((tableMatch = tableRegex.exec(expression)) !== null) { + const fullMatch = tableMatch[0]; + const tableName = tableMatch[1].trim().replace(/['"]/g, ''); + const rowNumber = tableMatch[2].trim(); + const fieldName = tableMatch[3].trim().replace(/['"]/g, ''); + + const replacement = \`(function() { + try { + if (!frm.doc["\${tableName}"] || !frm.doc["\${tableName}"][\${rowNumber} - 1]) { + return 0; + } + const value = frm.doc["\${tableName}"][\${rowNumber} - 1]["\${fieldName}"]; + return parseFloat(value) || 0; + } catch (e) { + return 0; + } + })()\`; + + transformed = transformed.replace(fullMatch, replacement); + } + + // === Обработка universal полей === + ${Object.keys(universal_fields).map(fieldId => { + const field = universal_fields[fieldId]; + if (!field || !field.name) return ''; + return `transformed = transformed.replace(/univ\\(${field.name}\\)/g, "(frm.doc.__universalFields && frm.doc.__universalFields['${fieldId}'] || 0)");`; + }).filter(Boolean).join('\n')} + + // === Обработка val() и row() === + const tablePrefixes = ["items", "taxes", "packed_items", "payments", "advances", "timesheets", "payment_schedule", "sales_team"]; + + for (const prefix of tablePrefixes) { + const sumPattern = new RegExp(\`sum\\\\(\${prefix}_([^)]+)\\\\)\`, 'g'); + transformed = transformed.replace(sumPattern, (m, f) => + \`(frm.doc["\${prefix}"] && frm.doc["\${prefix}"].reduce((s, r) => s + (parseFloat(r["\${f}"]) || 0), 0) || 0)\` + ); + + const valPattern = new RegExp(\`val\\\\(\${prefix}_([^)]+)\\\\)\`, 'g'); + transformed = transformed.replace(valPattern, (m, f) => + \`(frm.doc["\${prefix}"] && frm.doc["\${prefix}"].reduce((s, r) => s + (parseFloat(r["\${f}"]) || 0), 0) || 0)\` + ); + } + + if (for_child_table) { + transformed = transformed.replace(/row\\(([^)]+)\\)/g, (m, f) => + \`(parseFloat(row["\${f}"]) || 0)\` + ); + + transformed = transformed.replace(/val\\(([^)]+)\\)/g, (m, f) => { + if (f.startsWith(table_name + '_')) { + return \`(parseFloat(row["\${f.slice(table_name.length + 1)}"]) || 0)\`; + } + return \`(parseFloat(frm.doc["\${f}"]) || 0)\`; + }); + } else { + transformed = transformed.replace(/val\\(([^)]+)\\)/g, (m, f) => + \`(parseFloat(frm.doc["\${f}"]) || 0)\` + ); + + transformed = transformed.replace(/row\\(([^)]+)\\)/g, () => + \`0 /* ERROR: row() вне табличной части */\` + ); + } + + return { transformed, needsAsync, reportCalls }; + }`; +} + +function generateTableRowAccessFunction() { + return ` +// Функция для получения индекса строки по её имени (name/cdn) +function get_row_index(frm, tableName, rowName) { + + // Проверяем наличие таблицы + if (!frm.doc[tableName]) { + return -1; + } + + // Выводим все строки для отладки + frm.doc[tableName].forEach((row, idx) => { + }); + + // Ищем индекс строки по имени + for (let i = 0; i < frm.doc[tableName].length; i++) { + if (frm.doc[tableName][i].name === rowName) { + return i; + } + } + return -1; +} + +// Функция для доступа к полям конкретных строк таблицы +function table_row(tableName, rowIndex, fieldName) { + // Проверяем наличие необходимых аргументов + if (!tableName || rowIndex === undefined || !fieldName) { + return 0; + } + + // Преобразуем rowIndex в число (на случай, если передана строка) + const rowIdx = parseInt(rowIndex) - 1; // Индексация с 1 для пользователя, но с 0 в массиве + + try { + // Проверяем наличие таблицы и строки + if (!frm.doc[tableName] || !frm.doc[tableName][rowIdx]) { + return 0; + } + + // Получаем значение поля + const value = frm.doc[tableName][rowIdx][fieldName]; + + // Проверяем на NaN и возвращаем значение или 0 + const numValue = parseFloat(value); + if (isNaN(numValue)) { + return 0; + } + return numValue; + } catch (error) { + return 0; + } +} + +// Функция для обновления значения поля конкретной строки таблицы +function set_table_row(frm, tableName, rowIndex, fieldName, value) { + // Проверяем наличие необходимых аргументов + if (!frm || !tableName || rowIndex === undefined || !fieldName) { + return false; + } + + // Преобразуем rowIndex в число (на случай, если передана строка) + const rowIdx = parseInt(rowIndex) - 1; // Индексация с 1 для пользователя, но с 0 в массиве + + try { + // Проверяем наличие таблицы и строки + if (!frm.doc[tableName] || !frm.doc[tableName][rowIdx]) { + return false; + } + + // Получаем строку таблицы + const row = frm.doc[tableName][rowIdx]; + + // Получаем сетевое имя cdt и cdn для Frappe + const cdt = frm.fields_dict[tableName].grid.doctype; + const cdn = row.name; + + // Проверяем, является ли значение NaN, и если да, заменяем его на 0 + let finalValue = value; + if (typeof value === 'number' && isNaN(value)) { + finalValue = 0; + } + + // Обновляем значение поля + frappe.model.set_value(cdt, cdn, fieldName, finalValue); + + return true; + } catch (error) { + return false; + } +} + +// Функция для поиска строки таблицы по значению поля +function find_table_row(frm, tableName, fieldName, value) { + // Проверяем наличие необходимых аргументов + if (!frm || !tableName || !fieldName) { + return -1; + } + + try { + // Проверяем наличие таблицы + if (!frm.doc[tableName]) { + return -1; + } + + // Ищем строку с указанным значением поля + for (let i = 0; i < frm.doc[tableName].length; i++) { + if (frm.doc[tableName][i][fieldName] == value) { + // Возвращаем индекс строки (начиная с 1 для пользователя) + return i + 1; + } + } + + // Если строка не найдена + return -1; + } catch (error) { + return -1; + } +}`; +} + +function generate_child_table_script(table_child_scripts, table_doctypes, table_field_triggers, formula_data, functions) { + let child_scripts = ''; + + Object.keys(table_child_scripts).forEach(tableName => { + let childDoctype = table_child_scripts[tableName].doctype; + let usesUniversalFields = table_child_scripts[tableName].uses_universal; + let table_script = `\n\nfrappe.ui.form.on("${childDoctype}", {\n`; + + // Создаем общую функцию для расчета формул в строке таблицы + let calcFunction = ` + async function calculate_${tableName}_row(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + try { + ${usesUniversalFields ? 'await calculate_universal_fields(frm);' : ''} + + const { transformed, needsAsync, reportCalls } = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}"); + + let finalExpression = transformed; + + if (reportCalls && reportCalls.length > 0) { + const reportResults = []; + + for (const call of reportCalls) { + const result = await getReportFieldValue( + frm, + call.reportName, + call.filters, + call.fieldName, + call.aggregation + ); + reportResults.push({ placeholder: call.placeholder, value: result }); + } + + for (const result of reportResults) { + finalExpression = finalExpression.replace(result.placeholder, result.value); + } + } + + let result = 0; + try { + result = eval(finalExpression); + } catch (e) { + } + + // Обновляем значение + row["${calc.field}"] = isNaN(result) ? 0 : result; + frm.refresh_field("${tableName}"); + } catch (error) { + } + }`; + + table_child_scripts[tableName].calculations.forEach(calc => { + calcFunction += ` + // Расчет для поля ${calc.field} + try { + const transformed = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}").transformed; + row["${calc.field}"] = eval(transformed); + } catch (e) { + row["${calc.field}"] = 0; + } + `; + }); + + calcFunction += ` + frm.refresh_field("${tableName}"); + } catch (e) { + } + }`; + + functions.push(calcFunction); + + // Добавляем обработчики для каждого триггерного поля + Array.from(table_child_scripts[tableName].fields).forEach(field => { + if (usesUniversalFields) { + table_script += ` "${field}": async function(frm, cdt, cdn) { await calculate_${tableName}_row(frm, cdt, cdn); },\n`; + } else { + table_script += ` "${field}": function(frm, cdt, cdn) { calculate_${tableName}_row(frm, cdt, cdn); },\n`; + } + }); + + table_script += `});`; + child_scripts += table_script; + }); + + return child_scripts; +} + diff --git a/formula_editor/public/js/formula_editor_modals.js b/formula_editor/public/js/formula_editor_modals.js new file mode 100644 index 0000000..8a98cd0 --- /dev/null +++ b/formula_editor/public/js/formula_editor_modals.js @@ -0,0 +1,689 @@ +function createModals() { + createFormulaBuilderModal(); + createTriggerFieldsModal(); + createUniversalFieldsModal(); + createReportFieldModal(); + createTableRowFormulaModal(); + createTableRowFieldSelectorModal(); + createTableFieldSelectorModal(); +} + +function createFormulaBuilderModal() { +if ($("#formula_builder_modal").length) { + return; +} + +const modalHtml = ` + +`; + +// Стили с поддержкой перетаскивания +const modalStyles = ` + +`; + +$('body').append(modalHtml); +$('head').append(modalStyles); +} + +function createTriggerFieldsModal() { + if ($("#trigger_fields_modal").length) { + return; + } + + const modalHtml = ` + + `; + + $('body').append(modalHtml); +} + +function createUniversalFieldsModal() { + if ($("#universal_fields_modal").length) { + return; + } + + const modalHtml = ` + `; + + $('body').append(modalHtml); +} + +function createReportFieldModal() { + if ($("#report_fields_modal").length) { + return; + } + + const modalHtml = ` + `; + + $('body').append(modalHtml); +} + +function createTableRowFormulaModal() { + if ($("#table_row_formula_modal").length) { + return; + } + + const modalHtml = ` + + `; + + $('body').append(modalHtml); + + // Обработчики для селекторов + $("#table_row_table").on('change', function() { + const selectedTable = $(this).val(); + if (selectedTable) { + loadTableFieldsForSelector(selectedTable); + $("#table_row_field").prop('disabled', false); + } else { + $("#table_row_field").prop('disabled', true).empty().append(''); + } + }); + + // Добавляем обработчик для кнопки "При открытии документа" + $("#table_row_formula_modal").on('click', '.add-predefined-event', function() { + const $triggerContainer = $("#table_row_formula_modal").find(".table-row-trigger"); + // Используем специальную функцию для добавления предопределенного события + addTokenToTriggers("При открытии документа", $triggerContainer, 'event'); + }); + + // Добавляем обработчик для кнопки "Выбрать поле" + $("#table_row_formula_modal").on('click', '.open-trigger-editor-in-row', function() { + const $triggerContainer = $("#table_row_formula_modal").find(".table-row-trigger"); + const frm = cur_frm; + + // Открываем стандартный редактор триггеров, используя контейнер из модального окна строки таблицы + openTriggerFieldsEditorForTableRow(frm, $triggerContainer); + }); +} + +function createTableRowFieldSelectorModal() { + if ($("#table_row_field_selector_modal").length) { + return; + } + + const modalHtml = ` + + `; + + $('body').append(modalHtml); + + // Обработчики для селекторов + $("#row_field_table").on('change', function() { + const selectedTable = $(this).val(); + if (selectedTable) { + loadTableFieldsForSelector(selectedTable); + $("#row_field_name").prop('disabled', false); + } else { + $("#row_field_name").prop('disabled', true).empty().append(''); + } + }); +} + +function createTableFieldSelectorModal() { + if ($("#table_field_selector_modal").length) { + return; + } + + const modalHtml = ` + + `; + + $('body').append(modalHtml); + + // Обработчики для селекторов + $("#table_field_table").on('change', function() { + const selectedTable = $(this).val(); + if (selectedTable) { + loadTableFieldsForTableSelector(selectedTable); + $("#table_field_name").prop('disabled', false); + } else { + $("#table_field_name").prop('disabled', true).empty().append(''); + } + }); +} + +function loadTableFieldsForTableSelector(tableName) { + const $fieldSelect = $("#table_field_name"); + $fieldSelect.empty().append(''); + + const tableDoctypes = $("#table_field_selector_modal").data("table-doctypes") || {}; + const doctype = tableDoctypes[tableName]; + + if (!doctype) return; + + // Получаем поля DocType табличной части + frappe.model.with_doctype(doctype, function() { + const meta = frappe.get_meta(doctype); + if (!meta) return; + + // Добавляем все поля, которые могут быть использованы в формулах + const validTypes = ['Int', 'Float', 'Currency', 'Data', 'Text', 'Date', 'Datetime', 'Check', 'Link', 'Select', 'Percent']; + meta.fields.forEach(field => { + if (field.fieldname && validTypes.includes(field.fieldtype)) { + $fieldSelect.append(``); + } + }); + }); +} + +function loadTablesListForTableSelector(frm) { + const $tableSelect = $("#table_field_table"); + $tableSelect.empty().append(''); + + // Получаем все табличные части DocType + frappe.model.with_doctype(frm.doc.target_doctype, function() { + const meta = frappe.get_meta(frm.doc.target_doctype); + if (!meta) return; + + const tableDoctypes = {}; + + meta.fields.forEach(field => { + if (field.fieldtype === 'Table' && field.options) { + $tableSelect.append(``); + tableDoctypes[field.fieldname] = field.options; + } + }); + + // Сохраняем информацию о DocType для табличных частей + $("#table_field_selector_modal").data("table-doctypes", tableDoctypes); + }); +} + +function openTableFieldSelector(frm, targetContainer, isForTrigger = false) { + const $modal = $("#table_field_selector_modal"); + + // Сбрасываем форму + $("#table_field_table").val(''); + $("#table_field_name").empty().append('').prop('disabled', true); + $("#table_field_row").val(1); + + // Удаляем старые обработчики + $modal.find('.add-table-field').off('click'); + + // Загружаем список табличных частей + loadTablesListForTableSelector(frm); + + // Сохраняем информацию о целевом контейнере и типе (формула или триггер) + $modal.data("target-container", targetContainer); + $modal.data("is-trigger", isForTrigger); + $modal.data("frm", frm); + + // Обработчик для кнопки добавления + $modal.find('.add-table-field').on('click', function() { + const tableName = $("#table_field_table").val(); + const fieldName = $("#table_field_name").val(); + const rowNumber = $("#table_field_row").val(); + + if (!tableName || !fieldName || !rowNumber) { + frappe.msgprint({ + title: __('Внимание'), + message: __('Выберите табличную часть, поле и укажите номер строки'), + indicator: 'orange' + }); + return; + } + + const $container = $($modal.data("target-container")); + const isForTrigger = $modal.data("is-trigger"); + + if (isForTrigger) { + // Для триггеров добавляем идентификатор поля + const fieldId = `${tableName}_${fieldName}`; + addTokenToTriggers(fieldId, $container, 'table-field'); + } else { + // Для формул добавляем вызов функции table() + const formulaText = `table(${tableName}, ${rowNumber}, ${fieldName})`; + const $token = $(`${formulaText}×`); + $container.append($token).append(' '); + + // Добавляем обработчики для токенов + addTokenHandlers($container); + + // Переносим курсор в конец контейнера + setEndOfContentEditable($container[0]); + } + + $modal.modal('hide'); + }); + + // Показываем модальное окно + $modal.modal('show'); +} \ No newline at end of file diff --git a/formula_editor/public/js/formula_editor_reports.js b/formula_editor/public/js/formula_editor_reports.js new file mode 100644 index 0000000..61b3309 --- /dev/null +++ b/formula_editor/public/js/formula_editor_reports.js @@ -0,0 +1,1390 @@ +function initReportFields(frm) { + // Если хранилище полей отчетов не существует, создаем его + if (!frm.doc.report_fields_storage) { + frm.set_value("report_fields_storage", "{}"); + } + + // Инициализируем обработчики событий + attachReportFieldsEvents(frm); + + // Отображаем существующие поля отчетов + renderReportFields(frm); +} + +function attachReportFieldsEvents(frm) { + // Сначала удаляем существующие обработчики событий + $(frm.wrapper).off('click', '.add-report-field'); + $(frm.wrapper).off('click', '.edit-report-field'); + $(frm.wrapper).off('click', '.delete-report-field'); + + // Обработчик нажатия на кнопку "Добавить поле" + $(frm.wrapper).on('click', '.add-report-field', function() { + openReportFieldModal(frm); + }); + + // Обработчик для клика по кнопке редактирования + $(frm.wrapper).on('click', '.edit-report-field', function(e) { + e.stopPropagation(); + const fieldId = $(this).data('field-id'); + editReportField(frm, fieldId); + }); + + // Обработчик для клика по кнопке удаления + $(frm.wrapper).on('click', '.delete-report-field', function(e) { + e.stopPropagation(); + const fieldId = $(this).data('field-id'); + deleteReportField(frm, fieldId); + }); +} + +function createReportFieldModal() { + if ($("#report_fields_modal").length) { + return; + } + + const modalHtml = ` + `; + + $('body').append(modalHtml); +} + +function openReportFieldModal(frm, fieldId = null, fieldCopy = null) { + // Создаем/обновляем модальное окно + createReportFieldModal(); + + // Получаем значения для редактирования + let fieldValues = fieldCopy || {}; + if (!fieldCopy && fieldId) { + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + fieldValues = reportFields[fieldId] || {}; + } + + // Создаем диалог с формой для настройки поля отчета + const d = new frappe.ui.Dialog({ + title: fieldId ? 'Редактирование поля отчета' : 'Создание поля отчета', + fields: [ + { + label: 'Название поля', + fieldname: 'name', + fieldtype: 'Data', + reqd: 1, + default: fieldValues.name || '' + }, + { + label: 'Отчет', + fieldname: 'report', + fieldtype: 'Link', + options: 'Report', + reqd: 1, + default: fieldValues.report || '', + filters: { + "report_type": "Script Report" + }, + onchange: function() { + const selectedReport = d.get_value('report'); + + 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(); + } + } + }); + } 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(); + } + } + }, + { + label: 'Фильтры отчета', + fieldname: 'filters_section', + fieldtype: 'Section Break', + hidden: 1 + }, + { + fieldname: 'filters_helper', + fieldtype: 'HTML', + options: '
Настройте фильтры для отчета. Эти фильтры будут использоваться при каждом вычислении формулы.
', + hidden: 1 + }, + { + fieldname: 'filters_container', + fieldtype: 'HTML', + options: '
' + }, + { + fieldname: 'filters_json', + fieldtype: 'Code', + 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 + }, + { + label: 'Поле отчета', + fieldname: 'field_section', + fieldtype: 'Section Break' + }, + { + label: 'Поле отчета', + fieldname: 'field_name', + fieldtype: 'Select', + options: [], + reqd: 1, + default: fieldValues.field_name || '' + }, + { + label: 'Агрегация', + fieldname: 'aggregation', + fieldtype: 'Select', + options: [ + {label: 'Сумма', value: '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({ + title: __('Внимание'), + message: __('Заполните все обязательные поля'), + indicator: 'orange' + }); + return; + } + + // Обновляем значение фильтров + values.filters_json = filters_json; + + // Сохраняем данные + saveReportField(frm, values, fieldId); + + d.hide(); + } + }); + + // Сохраняем информацию о специальных датах + d.has_special_dates = fieldValues.__has_special_dates; + + // Показываем диалог + d.show(); + + // Если редактируем существующее поле, загружаем данные + 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]; + + // Если есть контрол для этого поля + 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; // Останавливаем цикл + } + }); + } + } + } + } + }); + } catch (e) { + } +} + +function generateFiltersHtml(filters, currentValues) { + let html = '
'; + + filters.forEach(filter => { + const fieldId = `filter_${filter.fieldname}`; + const currentValue = currentValues[filter.fieldname] || filter.default; + + html += ` +
+
${filter.label || filter.fieldname}
+
+
`; + }); + + html += '
'; + return html; +} + +function initFilterControls(d, currentValues) { + if (!d.report_filters) { + frappe.throw("Отсутствует информация о фильтрах отчета"); + return; + } + + // Инициализируем объект для хранения контролов + 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; + } + + 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; + + // Создаем контрол в зависимости от типа поля + 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; + + 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(); + } + }); + } 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; + } + + // Обработчик изменения для обновления JSON + $container.find('input').on('change', function() { + updateFiltersJson(d); + }); + } + 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); + }; + + // Сохраняем контрол + d.filter_controls[filter.fieldname] = control; + } + }); + + // Обновляем JSON фильтров + 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; + } + + // Показываем какие фильтры мы пытаемся использовать + frappe.show_alert({ + message: `Загружаем поля отчета '${reportName}' с фильтрами: ${JSON.stringify(filters)}`, + indicator: 'blue' + }); + + // Загружаем информацию о фильтрах отчета, если ее еще нет + 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 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')); + } + } else { + // Если полей нет, очищаем селект + d.set_df_property('field_name', 'options', []); + d.fields_dict.field_name.refresh(); + + frappe.throw(`В отчете '${reportName}' не найдено подходящих числовых полей. Пожалуйста, выберите другой отчет.`); + } + } + }); + } +} + +function saveReportField(frm, values, fieldId = null) { + // Используем имя поля в качестве идентификатора или указанный ID + const newFieldId = fieldId || frappe.utils.get_random(10); + + try { + let reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + + // Проверка на дублирование имени поля + const existingField = Object.keys(reportFields).find(key => + key !== newFieldId && reportFields[key].name === values.name + ); + + if (existingField) { + frappe.msgprint({ + title: __('Внимание'), + message: __('Поле с таким именем уже существует.'), + indicator: 'orange' + }); + 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, + report: values.report, + filters_json: values.filters_json, + field_name: values.field_name, + aggregation: values.aggregation + }; + + frm.set_value("report_fields_storage", JSON.stringify(reportFields)); + + frappe.show_alert({ + message: __('Поле отчета сохранено'), + indicator: 'green' + }); + + renderReportFields(frm); + } catch (error) { + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось сохранить поле отчета: ') + error.message, + indicator: 'red' + }); + } +} + +function renderReportFields(frm) { + const $container = $(frm.wrapper).find('.report-fields-list'); + $container.empty(); + + try { + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + + if (Object.keys(reportFields).length === 0) { + $container.append('
Нет полей отчетов. Нажмите "Добавить поле отчета" для создания.
'); + return; + } + + Object.keys(reportFields).forEach(fieldId => { + const field = reportFields[fieldId]; + + const $fieldButton = $(` +
+ + +
+ `); + + // Показываем/скрываем кнопки действий при наведении + $fieldButton.hover( + function() { $(this).find('.field-actions').show(); }, + function() { $(this).find('.field-actions').hide(); } + ); + + $container.append($fieldButton); + }); + + // Инициализируем drag-n-drop для полей отчетов + $container.find('.report-field-btn').each(function() { + $(this).attr('draggable', 'true'); + + $(this).off('dragstart').on('dragstart', function(e) { + const fieldId = $(this).data('field-id'); + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + const field = reportFields[fieldId]; + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'report-field', + fieldId: fieldId, + name: field.name + })); + }); + }); + } catch (error) { + $container.append('
Ошибка при загрузке полей отчетов.
'); + } +} + +function editReportField(frm, fieldId) { + try { + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + const field = reportFields[fieldId]; + + if (!field) { + frappe.throw("Поле отчета не найдено"); + return; + } + + // Важно: создаем копию поля, чтобы не изменять оригинал в хранилище + const fieldCopy = {...field}; + + // Обрабатываем фильтры перед открытием диалога + if (fieldCopy.filters_json) { + try { + let filters = JSON.parse(fieldCopy.filters_json); + const processedFilters = {}; + + // Проходим по всем фильтрам и обрабатываем специальные значения + Object.keys(filters).forEach(key => { + let value = filters[key]; + + // Обработка дат для месяца (ay) + if (key.toLowerCase().includes('date') && value === "$AyIlSpecial") { + // Заменяем специальное значение на обычную дату для временного использования + // Потом мы восстановим специальное значение при сохранении + processedFilters["__original_" + key] = "$AyIlSpecial"; // Сохраняем оригинальное значение + processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата + } + // Обработка дат для квартала (rüb) + else if (key.toLowerCase().includes('date') && value === "$RubIlSpecial") { + // Аналогично сохраняем оригинальное значение для квартала + processedFilters["__original_" + key] = "$RubIlSpecial"; // Сохраняем оригинальное значение + processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата + } + // Обработка ссылок на поля документа + else if (typeof value === 'string' && value.startsWith("doc.") && + (key.toLowerCase().includes('date') || key.toLowerCase().includes('from') || key.toLowerCase().includes('to'))) { + // Обрабатываем также ссылки на поля документа + processedFilters["__original_" + key] = value; // Сохраняем оригинальное значение + processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата + } + // Обычные значения без обработки + else { + processedFilters[key] = value; + } + }); + + // Обновляем JSON фильтров в копии + fieldCopy.filters_json = JSON.stringify(processedFilters); + fieldCopy.__has_special_dates = true; // Маркер для последующего восстановления + } catch (e) { + } + } + + // Открываем модальное окно с обработанной копией поля + openReportFieldModal(frm, fieldId, fieldCopy); + } catch (error) { + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось открыть поле отчета для редактирования: ') + error.message, + indicator: 'red' + }); + } +} + +function deleteReportField(frm, fieldId) { + frappe.confirm( + 'Вы уверены, что хотите удалить это поле отчета?', + function() { + try { + let reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + delete reportFields[fieldId]; + frm.set_value("report_fields_storage", JSON.stringify(reportFields)); + + // Перерисовываем список полей + renderReportFields(frm); + + frappe.show_alert({ + message: __('Поле отчета удалено'), + indicator: 'green' + }); + } catch (error) { + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось удалить поле отчета.'), + indicator: 'red' + }); + } + } + ); +} + +function generateReportFieldsFunction(report_fields) { + + + return ` + window.notifiedAboutAyIl = false; + window.notifiedAboutRubIl = false; +async function getReportFieldValue(frm, reportName, filters, fieldName, aggregation = 'sum') { + try { + + // Проверка необходимости полей сразу для всех фильтров + if (Object.values(filters).some(v => v === "$AyIlSpecial")) { + if (!frm.doc["ay"] || !frm.doc["il"]) { + if (!notifiedAboutAyIl) { + frappe.msgprint({ + title: "Not enough data", + message: "Please fill in the ay and il fields", + indicator: "orange" + }); + notifiedAboutAyIl = true; + } + return 0; + } + } + + // Проверка: если используется $RubIlSpecial, но rüb или il не заполнены — возвращаем 0 + if (Object.values(filters).some(v => v === "$RubIlSpecial")) { + if (!frm.doc["rüb"] || !frm.doc["il"]) { + if (!notifiedAboutRubIl) { + frappe.msgprint({ + title: "Not enough data", + message: "Please fill in the rüb and il fields", + indicator: "orange" + }); + notifiedAboutRubIl = true; + } + return 0; + } + } + + // Обрабатываем фильтры с возможными ссылками на поля документа + const processedFilters = {}; + + // Используем for...in loop для перебора свойств объекта filters + for (const key in filters) { + let value = filters[key]; + + // Специальная обработка для $DefaultCompany + if (value === "$DefaultCompany") { + processedFilters[key] = frm.doc.company || frappe.defaults.get_user_default("Company") || ""; + continue; + } + + // Специальная обработка для $AyIlSpecial + if (value === "$AyIlSpecial") { + // Проверяем, есть ли у документа поля ay и il + if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { + // Обрабатываем специальные поля ay и il для получения диапазона дат + const dateRange = processAzerbayjanDateField({ + ay: frm.doc["ay"], + il: frm.doc["il"] + }); + + // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия + if (dateRange) { + if (key.includes('from_date')) { + // Для начальной даты используем только >= + processedFilters[key] = dateRange.start; + } else if (key.includes('to_date')) { + // Для конечной даты используем только <= + processedFilters[key] = dateRange.end; + } else { + // Для других полей даты используем начало периода + processedFilters[key] = dateRange.start; + } + continue; + } + } + } + + // Специальная обработка для $RubIlSpecial + if (value === "$RubIlSpecial") { + // Проверяем, есть ли у документа поля rüb и il + if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { + // Обрабатываем специальные поля rüb и il для получения диапазона дат + const dateRange = processQuarterDateField({ + rüb: frm.doc["rüb"], + il: frm.doc["il"] + }); + + // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия + if (dateRange) { + if (key.includes('from_date')) { + // Для начальной даты используем только >= + processedFilters[key] = dateRange.start; + } else if (key.includes('to_date')) { + // Для конечной даты используем только <= + processedFilters[key] = dateRange.end; + } else { + // Для других полей даты используем начало периода + processedFilters[key] = dateRange.start; + } + continue; + } + } + } + + // Проверяем, является ли значение ссылкой на поле документа + if (typeof value === 'string' && value.startsWith('doc.')) { + const docField = value.substring(4); // Убираем 'doc.' + + // Проверяем, является ли это специальным полем ay или il + if (docField === 'ay' || docField === 'il') { + // Получаем значения ay и il из документа + if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { + // Обрабатываем специальные поля ay и il для получения диапазона дат + const dateRange = processAzerbayjanDateField({ + ay: frm.doc["ay"], + il: frm.doc["il"] + }); + + // Если удалось получить диапазон дат, создаем правильные фильтры + if (dateRange) { + if (key.includes('from_date')) { + // Для начальной даты используем только >= + processedFilters[key] = dateRange.start; + } else if (key.includes('to_date')) { + // Для конечной даты используем только <= + processedFilters[key] = dateRange.end; + } else { + // Для других полей даты используем начало периода + processedFilters[key] = dateRange.start; + } + continue; + } + } + } + // Проверяем, является ли это специальным полем rüb + else if (docField === 'rüb') { + // Получаем значения rüb и il из документа + if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { + // Обрабатываем специальные поля rüb и il для получения диапазона дат + const dateRange = processQuarterDateField({ + rüb: frm.doc["rüb"], + il: frm.doc["il"] + }); + + // Если удалось получить диапазон дат, создаем правильные фильтры + if (dateRange) { + if (key.includes('from_date')) { + // Для начальной даты используем только >= + processedFilters[key] = dateRange.start; + } else if (key.includes('to_date')) { + // Для конечной даты используем только <= + processedFilters[key] = dateRange.end; + } else { + // Для других полей даты используем начало периода + processedFilters[key] = dateRange.start; + } + continue; + } + } + } + // Для других полей документа + else if (frm.doc[docField] !== undefined) { + // Берем значение из документа + processedFilters[key] = frm.doc[docField]; + continue; + } else { + // Если поле не найдено, пропускаем фильтр + continue; + } + } + + // Добавляем обработанное значение в фильтры + processedFilters[key] = value; + } + + // Проверяем кеш для экономии запросов + if (!window.reportCache) window.reportCache = {}; + + const cacheKey = \`\${reportName}|\${JSON.stringify(processedFilters)}|\${fieldName}|\${aggregation}\`; + + // Если есть в кеше и кеш не устарел (5 минут) + const now = new Date().getTime(); + if (window.reportCache[cacheKey] && + window.reportCache[cacheKey].timestamp > now - 5 * 60 * 1000) { + return window.reportCache[cacheKey].value; + } + + // Выполняем запрос к серверу с обработанными фильтрами + try { + const result = await frappe.call({ + method: "formula_editor.api.run_report_for_formula", + args: { + report_name: reportName, + filters: processedFilters, + field_name: fieldName + }, + freeze: false // Не блокируем интерфейс + }); + + // Проверяем результат + if (result && result.message) { + if (result.message.error) { + return 0; + } + + // Получаем числовое значение + let value = parseFloat(result.message); + if (isNaN(value)) value = 0; + + // Сохраняем в кеш + window.reportCache[cacheKey] = { + value: value, + timestamp: now + }; + + return value; + } + } catch (err) { + } + + return 0; + } catch (error) { + return 0; + } +}`; +} + +function updateFiltersJson(d) { + try { + // Собираем все значения фильтров + const values = {}; + Object.keys(d.filter_controls || {}).forEach(fieldname => { + const ctrl = d.filter_controls[fieldname]; + if (ctrl) { + try { + const value = ctrl.get_value(); + if (value !== undefined && value !== null && value !== '') { + // Сохраняем все значения как есть + values[fieldname] = value; + + // Проверка на специальные значения дат + if ((value === "$AyIlSpecial" || value === "$RubIlSpecial" || + (typeof value === 'string' && value.startsWith("doc."))) && + (fieldname.toLowerCase().includes('date') || + fieldname.toLowerCase().includes('from') || + fieldname.toLowerCase().includes('to'))) { + + // Сохраняем оригинальное значение отдельно + values[`__original_${fieldname}`] = value; + + // Временно заменяем значение на валидную дату для проверок + values[fieldname] = frappe.datetime.get_today(); + + // Отмечаем, что у нас есть специальные даты + d.set_value('has_special_dates', 1); + } + } + } catch (e) { + frappe.show_alert({ + message: `Ошибка при получении значения фильтра ${fieldname}: ${e.message}`, + indicator: 'red' + }); + } + } + }); + + // Обновляем JSON фильтров + if (Object.keys(values).length > 0) { + d.set_value('filters_json', JSON.stringify(values, null, 2)); + } + } catch (e) { + frappe.show_alert({ + message: `Ошибка при обновлении фильтров: ${e.message}`, + indicator: 'red' + }); + } +} \ No newline at end of file diff --git a/formula_editor/public/js/formula_editor_table_rows.js b/formula_editor/public/js/formula_editor_table_rows.js new file mode 100644 index 0000000..9c821c9 --- /dev/null +++ b/formula_editor/public/js/formula_editor_table_rows.js @@ -0,0 +1,1422 @@ +function initTableRowFormulas(frm) { + // Если хранилище формул не существует, создаем его + if (!frm.doc.table_row_formulas_storage) { + frm.set_value("table_row_formulas_storage", "{}"); + } + + // Создаем модальное окно, если его еще нет + createTableRowFormulaModal(); + + // Удаляем существующие обработчики + $(frm.wrapper).off('click', '.add-table-row-formula'); + $(frm.wrapper).off('click', '.edit-table-row-formula'); + $(frm.wrapper).off('click', '.delete-table-row-formula'); + + // Обработчик для кнопки добавления новой формулы + $(frm.wrapper).on('click', '.add-table-row-formula', function() { + openTableRowFormulaModal(frm); + }); + + // Обработчик для кнопки редактирования формулы + $(frm.wrapper).on('click', '.edit-table-row-formula', function() { + const formulaId = $(this).data('formula-id'); + openTableRowFormulaModal(frm, formulaId); + }); + + // Обработчик для кнопки удаления формулы + $(frm.wrapper).on('click', '.delete-table-row-formula', function() { + const formulaId = $(this).data('formula-id'); + deleteTableRowFormula(frm, formulaId); + }); + + // Отображаем существующие формулы + renderTableRowFormulas(frm); +} + +function createTableRowFormulaModal() { + if ($("#table_row_formula_modal").length) { + return; + } + + const modalHtml = ` + + `; + + $('body').append(modalHtml); + + // Обработчики для селекторов + $("#table_row_table").on('change', function() { + const selectedTable = $(this).val(); + if (selectedTable) { + loadTableFieldsForSelector(selectedTable); + $("#table_row_field").prop('disabled', false); + } else { + $("#table_row_field").prop('disabled', true).empty().append(''); + } + }); + + // Добавляем обработчик для кнопки "При открытии документа" + $("#table_row_formula_modal").on('click', '.add-predefined-event', function() { + const $triggerContainer = $("#table_row_formula_modal").find(".table-row-trigger"); + // Используем специальную функцию для добавления предопределенного события + addTokenToTriggers("При открытии документа", $triggerContainer, 'event'); + }); + + // Добавляем обработчик для кнопки "Выбрать поле" + $("#table_row_formula_modal").on('click', '.open-trigger-editor-in-row', function() { + const $triggerContainer = $("#table_row_formula_modal").find(".table-row-trigger"); + const frm = cur_frm; + + // Открываем стандартный редактор триггеров, используя контейнер из модального окна строки таблицы + openTriggerFieldsEditorForTableRow(frm, $triggerContainer); + }); +} + +function loadTablesList(frm) { + const $tableSelect = $("#table_row_table"); + $tableSelect.empty().append(''); + + // Получаем все табличные части DocType + frappe.model.with_doctype(frm.doc.target_doctype, function() { + const meta = frappe.get_meta(frm.doc.target_doctype); + if (!meta) return; + + const tableDoctypes = {}; + + meta.fields.forEach(field => { + if (field.fieldtype === 'Table' && field.options) { + $tableSelect.append(``); + tableDoctypes[field.fieldname] = field.options; + } + }); + + // Сохраняем информацию о DocType для табличных частей + $("#table_row_formula_modal").data("table-doctypes", tableDoctypes); + }); +} + +function loadTableFields(tableName) { + const $fieldSelect = $("#table_row_field"); + $fieldSelect.empty().append(''); + + const tableDoctypes = $("#table_row_formula_modal").data("table-doctypes") || {}; + const doctype = tableDoctypes[tableName]; + + if (!doctype) return; + + // Получаем поля DocType табличной части + frappe.model.with_doctype(doctype, function() { + const meta = frappe.get_meta(doctype); + if (!meta) return; + + // Фильтруем только числовые поля для формул + const numericTypes = ['Int', 'Float', 'Currency']; + meta.fields.forEach(field => { + if (field.fieldname && numericTypes.includes(field.fieldtype)) { + $fieldSelect.append(``); + } + }); + }); +} + +function openTableRowFormulaModal(frm, formulaId = null) { + const $modal = $("#table_row_formula_modal"); + const $resultContainer = $modal.find(".table-row-formula"); + const $triggerContainer = $modal.find(".table-row-trigger"); + + // Очистка предыдущих обработчиков и данных + $modal.off('click', '.field-btn'); + $modal.off('click', '.universal-field-btn'); + $modal.off('click', '.operator-btn'); + $modal.off('click', '.save-table-row-formula'); + $modal.off('click', '.add-predefined-event'); + $modal.off('click', '.open-trigger-editor-in-row'); + $modal.off('click', '.select-table-row-trigger-field'); + + $resultContainer.empty(); + $triggerContainer.empty(); + $modal.find(".field-groups-container").empty(); + + // ИСПРАВЛЕНИЕ: явно очищаем ID формулы, если это новая формула + if (!formulaId) { + $modal.removeData("formula-id"); + } + + // Показываем модальное окно с блокировкой контента + $modal.find(".modal-content").append(''); + $modal.modal('show'); + + // Используем обещания для последовательной загрузки данных + new Promise((resolve) => { + // Шаг 1: Получаем информацию о DocType таблиц + const table_doctypes = {}; + getTableDoctypesAsync(frm.doc.target_doctype, table_doctypes, () => { + // Сохраняем результат в глобальных данных модального окна + $modal.data("table_doctypes", table_doctypes); + resolve(); + }); + }) + .then(() => { + return new Promise((resolve) => { + // Шаг 2: Загружаем список табличных частей + loadTablesListAsync(frm, $modal, () => { + resolve(); + }); + }); + }) + .then(() => { + // Шаг 3: Если редактируем существующую формулу, загружаем ее данные + if (formulaId) { + return new Promise((resolve) => { + const tableRowFormulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); + const formula = tableRowFormulas[formulaId]; + + if (formula) { + // Заполняем селекты + $("#table_row_table").val(formula.table_name); + + // Загружаем поля выбранной таблицы + loadTableFieldsAsync(formula.table_name, $modal, () => { + $("#table_row_field").val(formula.field_name); + $("#table_row_index").val(formula.row_index); + + // Загружаем порядок выполнения (если существует, иначе значение по умолчанию 10) + $("#table_row_order").val(formula.order || 10); + + // Заполняем формулу и триггеры + parseAndRenderFormula($resultContainer, formula.formula, true); + parseAndRenderTriggers($triggerContainer, formula.triggers); + + $modal.data("formula-id", formulaId); + resolve(); + }); + } else { + resolve(); + } + }); + } + }) + .then(() => { + // Шаг 4: Загружаем поля для формулы + return new Promise((resolve) => { + populateTableRowFormulaFieldsAsync(frm, $modal, () => { + resolve(); + }); + }); + }) + .then(() => { + // Шаг 5: Настраиваем обработчики событий + + // Обработчики для операторов + $modal.find('.operator-btn').off('click').on('click', function() { + const operator = $(this).data('operator'); + addTokenToFormula(operator, 'operator', $resultContainer); + }); + + // Обработчик для кнопки сохранения + $modal.find('.save-table-row-formula').off('click').on('click', function() { + const tableName = $("#table_row_table").val(); + const fieldName = $("#table_row_field").val(); + const rowIndex = $("#table_row_index").val(); + + if (!tableName || !fieldName || !rowIndex) { + frappe.msgprint({ + title: __('Внимание'), + message: __('Выберите табличную часть, поле и укажите номер строки'), + indicator: 'orange' + }); + return; + } + + const formula = getFormulaText($resultContainer); + const triggers = getTriggerFieldsText($triggerContainer); + + // Сохраняем формулу со всеми необходимыми данными, включая порядок + saveTableRowFormula(frm, { + table_name: tableName, + field_name: fieldName, + row_index: rowIndex, + formula: formula, + triggers: triggers + }, $modal.data("formula-id")); + + $modal.modal('hide'); + }); + + // Обработчик для кнопки "При открытии документа" + $modal.find('.add-predefined-event').off('click').on('click', function() { + addTokenToTriggers("При открытии документа", $triggerContainer, 'event'); + }); + + // Обработчик для кнопки "Выбрать поле" + $modal.find('.open-trigger-editor-in-row').off('click').on('click', function() { + openTriggerFieldsEditorForTableRow(frm, $triggerContainer); + }); + + // Обработчик для кнопки выбора поля строки табличной части + $modal.find('.select-table-row-trigger-field').off('click').on('click', function() { + openTableRowFieldSelector(frm, $triggerContainer, true); + }); + + // Обработчики для кнопок полей (используем делегирование событий) + $modal.off('click', '.field-btn, .universal-field-btn').on('click', '.field-btn, .universal-field-btn', function() { + const fieldText = $(this).text().trim(); + const fieldType = $(this).hasClass('universal-field-btn') ? 'universal-field' : 'field'; + + // Добавляем в формулу + const $token = $(`${fieldText}×`); + $resultContainer.append($token).append(' '); + + // Добавляем обработчики для токенов + addTokenHandlers($resultContainer); + }); + + // Настраиваем перетаскивание и редактирование + initDragAndDrop($modal, $resultContainer, true, frm); + setupEditableContainer($resultContainer); + + // Настраиваем drag and drop для триггеров + initTriggerDragAndDrop($modal, $triggerContainer); + setupTriggerEditableContainer($triggerContainer); + + // ИСПРАВЛЕНИЕ: Добавляем обработчик закрытия модального окна для очистки данных + $modal.off('hidden.bs.modal').on('hidden.bs.modal', function() { + $modal.removeData("formula-id"); + }); + + // Удаляем блокирующий оверлей + $modal.find(".modal-overlay").remove(); + }) + .catch((error) => { + $modal.find(".modal-overlay").remove(); + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось загрузить данные для формулы строки таблицы'), + indicator: 'red' + }); + }); +} + +function getTableDoctypesAsync(target_doctype, result_object, callback) { + + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "DocType", + name: target_doctype + }, + callback: function(r) { + if (r.message && r.message.fields) { + r.message.fields.forEach(field => { + if (field.fieldtype === 'Table' && field.options) { + result_object[field.fieldname] = field.options; + } + }); + } + + // Если данных нет, используем альтернативный способ через meta + if (Object.keys(result_object).length === 0) { + + const meta = frappe.get_meta(target_doctype); + if (meta && meta.fields) { + meta.fields.forEach(field => { + if (field.fieldtype === 'Table' && field.options) { + result_object[field.fieldname] = field.options; + } + }); + } + } + + // Проверяем результат + if (Object.keys(result_object).length === 0) { + } else { + } + + if (callback) callback(); + } + }); +} + +function loadTablesListAsync(frm, $modal, callback) { + const $tableSelect = $("#table_row_table"); + $tableSelect.empty().append(''); + + // Получаем все табличные части DocType + frappe.model.with_doctype(frm.doc.target_doctype, function() { + const meta = frappe.get_meta(frm.doc.target_doctype); + if (!meta) { + if (callback) callback(); + return; + } + + const table_doctypes = $modal.data("table_doctypes") || {}; + + meta.fields.forEach(field => { + if (field.fieldtype === 'Table' && field.options) { + $tableSelect.append(``); + table_doctypes[field.fieldname] = field.options; + } + }); + + // Обновляем обработчик для выбора таблицы + $("#table_row_table").off('change').on('change', function() { + const selectedTable = $(this).val(); + if (selectedTable) { + loadTableFieldsAsync(selectedTable, $modal, null); + $("#table_row_field").prop('disabled', false); + } else { + $("#table_row_field").prop('disabled', true).empty().append(''); + } + }); + + if (callback) callback(); + }); +} + +function loadTableFieldsAsync(tableName, $modal, callback) { + const $fieldSelect = $("#table_row_field"); + $fieldSelect.empty().append(''); + + if (!tableName) { + if (callback) callback(); + return; + } + + const table_doctypes = $modal.data("table_doctypes") || {}; + const doctype = table_doctypes[tableName]; + + if (!doctype) { + if (callback) callback(); + return; + } + + // Получаем поля DocType табличной части + frappe.model.with_doctype(doctype, function() { + const meta = frappe.get_meta(doctype); + if (!meta) { + if (callback) callback(); + return; + } + + // Добавляем поля в селект + meta.fields.forEach(field => { + if (field.fieldname && ['Int', 'Float', 'Currency'].includes(field.fieldtype)) { + $fieldSelect.append(``); + } + }); + + // Загружаем поля строки для визуального редактора + populateRowFieldsAsync(tableName, $modal, doctype); + + if (callback) callback(); + }); +} + +function populateTableRowFormulaFieldsAsync(frm, $modal, callback) { + const $container = $modal.find(".field-groups-container"); + $container.empty(); + + // Получаем контейнер для формулы + const $formulaContainer = $modal.find(".table-row-formula"); + + // 1. Добавляем раздел с полями основного документа + const $documentFieldsGroup = $(` +
+
Поля документа
+
+
+ `); + + const $documentFieldsContainer = $documentFieldsGroup.find('.field-buttons-container'); + + // Получаем все поля основного документа + frappe.model.with_doctype(frm.doc.target_doctype, function() { + const meta = frappe.get_meta(frm.doc.target_doctype); + if (meta && meta.fields) { + meta.fields.forEach(field => { + if (field.fieldname && ['Data', 'Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)) { + const $fieldBtn = $(` + + `); + + $documentFieldsContainer.append($fieldBtn); + } + }); + } + + $container.append($documentFieldsGroup); + + // 2. Добавляем раздел с полями текущей строки таблицы + const $rowFieldsGroup = $(` +
+
Поля текущей строки
+
+
+ `); + + $container.append($rowFieldsGroup); + + // 3. Добавляем раздел с универсальными полями + if (frm.doc.universal_fields_storage) { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + if (Object.keys(universalFields).length > 0) { + const $universalFieldGroup = $(` +
+
Универсальные поля
+
+
+ `); + + const $universalButtonsContainer = $universalFieldGroup.find('.field-buttons-container'); + + Object.keys(universalFields).forEach(fieldId => { + const field = universalFields[fieldId]; + + if (!field || !field.name) return; + + const $fieldBtn = $(` + + `); + + $universalButtonsContainer.append($fieldBtn); + }); + + $container.append($universalFieldGroup); + } + } + + // 4. Добавляем раздел с полями отчетов + if (frm.doc.report_fields_storage) { + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + + if (Object.keys(reportFields).length > 0) { + const $reportFieldGroup = $(` +
+
Поля отчетов
+
+
+ `); + + const $reportButtonsContainer = $reportFieldGroup.find('.field-buttons-container'); + + Object.keys(reportFields).forEach(fieldId => { + const field = reportFields[fieldId]; + + if (!field || !field.name) return; + + const $fieldBtn = $(` + + `); + + $reportButtonsContainer.append($fieldBtn); + }); + + $container.append($reportFieldGroup); + } + } + + // НОВОЕ: 5. Добавляем группу для полей из конкретных строк табличных частей + const $tableFieldGroup = $(` +
+
Поля из строк табличных частей
+
+ +
+
+ `); + + $container.append($tableFieldGroup); + + // НОВОЕ: Добавляем обработчик для кнопки выбора поля из строки табличной части + $tableFieldGroup.find('.select-table-field-in-row').on('click', function() { + const targetContainer = $modal.find(".table-row-formula"); + openTableFieldSelector(frm, targetContainer, false); + }); + + // 6. Добавляем обработчики для кнопок полей + $modal.off('click', '.field-btn, .universal-field-btn, .report-field-btn').on('click', '.field-btn, .universal-field-btn, .report-field-btn', function() { + const fieldText = $(this).text().trim(); + const fieldType = $(this).hasClass('universal-field-btn') ? 'universal-field' : + $(this).hasClass('report-field-btn') ? 'report-field' : 'field'; + + // Добавляем в формулу + const $token = $(`${fieldText}×`); + $formulaContainer.append($token).append(' '); + + // Добавляем обработчики к токену + addTokenHandlers($formulaContainer); + }); + + if (callback) callback(); + }); +} + +function populateRowFieldsAsync(tableName, $modal, doctype) { + const $rowFieldsContainer = $modal.find('.row-fields-container'); + $rowFieldsContainer.empty(); + + if (!tableName || !doctype) return; + + frappe.model.with_doctype(doctype, function() { + const childMeta = frappe.get_meta(doctype); + if (childMeta && childMeta.fields) { + childMeta.fields.forEach(field => { + if (field.fieldname && ['Data', 'Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)) { + const $fieldBtn = $(` + + `); + + $rowFieldsContainer.append($fieldBtn); + } + }); + } + }); +} + +function populateTableRowFormulaFields($modal, frm) { + const $container = $modal.find(".field-groups-container"); + $container.empty(); + + // Получаем контейнер для формулы + const $formulaContainer = $modal.find(".table-row-formula"); + + // 1. Добавляем раздел с полями основного документа + const $documentFieldsGroup = $(` +
+
Поля документа
+
+
+ `); + + const $documentFieldsContainer = $documentFieldsGroup.find('.field-buttons-container'); + + // Получаем все поля основного документа + frappe.model.with_doctype(frm.doc.target_doctype, function() { + const meta = frappe.get_meta(frm.doc.target_doctype); + if (meta && meta.fields) { + meta.fields.forEach(field => { + if (field.fieldname && ['Data', 'Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)) { + const $fieldBtn = $(` + + `); + + $documentFieldsContainer.append($fieldBtn); + } + }); + } + }); + + $container.append($documentFieldsGroup); + + // 2. Добавляем раздел с полями текущей строки таблицы + const $rowFieldsGroup = $(` +
+
Поля текущей строки
+
+
+ `); + + $container.append($rowFieldsGroup); + + // Обработчик для выбора таблицы, чтобы показать поля строки + $("#table_row_table").on('change', function() { + const selectedTable = $(this).val(); + if (!selectedTable) return; + + const $rowFieldsContainer = $rowFieldsGroup.find('.field-buttons-container'); + $rowFieldsContainer.empty(); + + // Получаем DocType для выбранной таблицы + const table_doctypes = {}; + getTableDoctypes(frm.doc.target_doctype, table_doctypes); + const childDoctype = table_doctypes[selectedTable]; + + if (childDoctype) { + frappe.model.with_doctype(childDoctype, function() { + const childMeta = frappe.get_meta(childDoctype); + if (childMeta && childMeta.fields) { + childMeta.fields.forEach(field => { + if (field.fieldname && ['Data', 'Int', 'Float', 'Currency', 'Percent'].includes(field.fieldtype)) { + const $fieldBtn = $(` + + `); + + $rowFieldsContainer.append($fieldBtn); + } + }); + } + }); + } + }); + + // 3. Добавляем раздел с универсальными полями + if (frm.doc.universal_fields_storage) { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + if (Object.keys(universalFields).length > 0) { + const $universalFieldGroup = $(` +
+
Универсальные поля
+
+
+ `); + + const $universalButtonsContainer = $universalFieldGroup.find('.field-buttons-container'); + + Object.keys(universalFields).forEach(fieldId => { + const field = universalFields[fieldId]; + + if (!field || !field.name) return; + + const $fieldBtn = $(` + + `); + + $universalButtonsContainer.append($fieldBtn); + }); + + $container.append($universalFieldGroup); + } + } + + // 4. Добавляем обработчики для кнопок полей + $modal.off('click', '.field-btn, .universal-field-btn').on('click', '.field-btn, .universal-field-btn', function() { + const fieldText = $(this).text().trim(); + const fieldType = $(this).hasClass('universal-field-btn') ? 'universal-field' : 'field'; + + // Добавляем в формулу + const $token = $(`${fieldText}×`); + $formulaContainer.append($token).append(' '); + + // Добавляем обработчики к токену + addTokenHandlers($formulaContainer); + }); +} + +function saveTableRowFormula(frm, formulaData, formulaId = null) { + const newFormulaId = formulaId || frappe.utils.get_random(10); + + try { + let tableRowFormulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); + + // Получаем значение порядка из интерфейса + const order = parseInt($("#table_row_order").val()) || 10; + + // Добавляем порядок в данные формулы + formulaData.order = order; + + // Сохраняем новую или обновленную формулу + tableRowFormulas[newFormulaId] = formulaData; + + // Обновляем значение в форме + frm.set_value("table_row_formulas_storage", JSON.stringify(tableRowFormulas)); + + frappe.show_alert({ + message: __('Формула для строки табличной части сохранена'), + indicator: 'green' + }); + + // Обновляем список формул + renderTableRowFormulas(frm); + } catch (error) { + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось сохранить формулу для строки табличной части.'), + indicator: 'red' + }); + } +} + +function renderTableRowFormulas(frm) { + const $container = $(frm.wrapper).find('.table-row-formulas-list'); + $container.empty(); + + try { + const tableRowFormulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); + + if (Object.keys(tableRowFormulas).length === 0) { + $container.append('
Нет формул для строк табличных частей. Нажмите "Добавить формулу" для создания.
'); + return; + } + + // Создаем таблицу для списка формул + const $table = $(` + + + + + + + + + + + + + +
Табличная частьПолеСтрокаПорядокФормулаОбновлять при измененииДействия
+ `); + + const $tbody = $table.find('tbody'); + + // Добавляем формулы в таблицу + Object.keys(tableRowFormulas).forEach(formulaId => { + const formula = tableRowFormulas[formulaId]; + + const $row = $(` + + ${formula.table_name} + ${formula.field_name} + ${formula.row_index} + ${formula.order || 10} + ${formula.formula} + ${formula.triggers || '-'} + + + + + + `); + + $tbody.append($row); + }); + + $container.append($table); + } catch (error) { + $container.append('
Ошибка при загрузке формул для строк табличных частей.
'); + } +} + +function deleteTableRowFormula(frm, formulaId) { + frappe.confirm( + 'Вы уверены, что хотите удалить эту формулу для строки табличной части?', + function() { + try { + let tableRowFormulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); + delete tableRowFormulas[formulaId]; + frm.set_value("table_row_formulas_storage", JSON.stringify(tableRowFormulas)); + + // Перерисовываем список формул + renderTableRowFormulas(frm); + + frappe.show_alert({ + message: __('Формула для строки табличной части удалена'), + indicator: 'green' + }); + } catch (error) { + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось удалить формулу для строки табличной части.'), + indicator: 'red' + }); + } + } + ); +} + +function createTableRowFieldSelectorModal() { + if ($("#table_row_field_selector_modal").length) { + return; + } + + const modalHtml = ` + + `; + + $('body').append(modalHtml); + + // Обработчики для селекторов + $("#row_field_table").on('change', function() { + const selectedTable = $(this).val(); + if (selectedTable) { + loadTableFieldsForSelector(selectedTable); + $("#row_field_name").prop('disabled', false); + } else { + $("#row_field_name").prop('disabled', true).empty().append(''); + } + }); +} + +function loadTableFieldsForSelector(tableName) { + const $fieldSelect = $("#row_field_name"); + $fieldSelect.empty().append(''); + + const tableDoctypes = $("#table_row_field_selector_modal").data("table-doctypes") || {}; + const doctype = tableDoctypes[tableName]; + + if (!doctype) return; + + // Получаем поля DocType табличной части + frappe.model.with_doctype(doctype, function() { + const meta = frappe.get_meta(doctype); + if (!meta) return; + + // Добавляем все поля, которые могут быть использованы в формулах и триггерах + const validTypes = ['Int', 'Float', 'Currency', 'Data', 'Text', 'Date', 'Datetime', 'Check', 'Link', 'Select']; + meta.fields.forEach(field => { + if (field.fieldname && validTypes.includes(field.fieldtype)) { + $fieldSelect.append(``); + } + }); + }); +} + +function loadTablesListForSelector(frm) { + const $tableSelect = $("#row_field_table"); + $tableSelect.empty().append(''); + + // Получаем все табличные части DocType + frappe.model.with_doctype(frm.doc.target_doctype, function() { + const meta = frappe.get_meta(frm.doc.target_doctype); + if (!meta) return; + + const tableDoctypes = {}; + + meta.fields.forEach(field => { + if (field.fieldtype === 'Table' && field.options) { + $tableSelect.append(``); + tableDoctypes[field.fieldname] = field.options; + } + }); + + // Сохраняем информацию о DocType для табличных частей + $("#table_row_field_selector_modal").data("table-doctypes", tableDoctypes); + }); +} + +function openTableRowFieldSelector(frm, targetContainer, isForTrigger = false) { + const $modal = $("#table_row_field_selector_modal"); + + // Сбрасываем форму + $("#row_field_table").val(''); + $("#row_field_name").empty().append('').prop('disabled', true); + $("#row_field_index").val(1); + + // Удаляем старые обработчики + $modal.find('.add-table-row-field').off('click'); + + // Загружаем список табличных частей + loadTablesListForSelector(frm); + + // Сохраняем информацию о целевом контейнере и типе (формула или триггер) + $modal.data("target-container", targetContainer); + $modal.data("is-trigger", isForTrigger); + $modal.data("frm", frm); + + // Обработчик для кнопки добавления + $modal.find('.add-table-row-field').on('click', function() { + const tableName = $("#row_field_table").val(); + const fieldName = $("#row_field_name").val(); + const rowIndex = $("#row_field_index").val(); + + if (!tableName || !fieldName || !rowIndex) { + frappe.msgprint({ + title: __('Внимание'), + message: __('Выберите табличную часть, поле и укажите номер строки'), + indicator: 'orange' + }); + return; + } + + const fieldId = `${tableName}_row_${rowIndex}_${fieldName}`; + const $container = $($modal.data("target-container")); + const isForTrigger = $modal.data("is-trigger"); + + if (isForTrigger) { + // Добавляем поле как триггер + addTokenToTriggers(fieldId, $container, 'table-row'); + } else { + // Добавляем поле в формулу + addTokenToFormula(fieldId, 'field', $container); + } + + $modal.modal('hide'); + }); + + // Показываем модальное окно + $modal.modal('show'); +} + +function generateTableRowFormulasHandlers(table_row_formulas, functions, target_doctype, universal_fields) { + // Создаем функцию для инициализации формул строк табличных частей + let initFunction = ` + // Функция для инициализации формул строк табличных частей + function initialize_table_row_formulas(frm) { + try { + // Получаем информацию о формулах для строк табличных частей + const formulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}"); + + // ВАЖНО: Преобразуем объект в массив и сортируем его по порядку выполнения + const sortedFormulas = Object.entries(formulas).map(([id, formula]) => { + return { id, ...formula }; + }).sort((a, b) => { + // Сортировка по значению поля order (если порядок не задан, используем 10 по умолчанию) + const orderA = a.order !== undefined ? parseInt(a.order) : 10; + const orderB = b.order !== undefined ? parseInt(b.order) : 10; + return orderA - orderB; + }); + + // Группируем формулы по табличным частям и строкам + const formulasByTableAndRow = {}; + + sortedFormulas.forEach(formula => { + const tableName = formula.table_name; + const rowIndex = parseInt(formula.row_index); + + if (!formulasByTableAndRow[tableName]) { + formulasByTableAndRow[tableName] = {}; + } + + if (!formulasByTableAndRow[tableName][rowIndex]) { + formulasByTableAndRow[tableName][rowIndex] = []; + } + + formulasByTableAndRow[tableName][rowIndex].push(formula); + }); + + // Устанавливаем обработчики для каждой табличной части + Object.keys(formulasByTableAndRow).forEach(tableName => { + if (!frm.doc[tableName]) { + return; + } + + // Для каждой строки таблицы + frm.doc[tableName].forEach((row, rowIdx) => { + // Получаем отсортированные формулы для текущей строки (строки нумеруются с 1, а индексы с 0) + const rowFormulas = formulasByTableAndRow[tableName][rowIdx + 1] || []; + + // Выполняем формулы в порядке сортировки (по полю order) + for (const formula of rowFormulas) { + try { + // Определяем необходимость асинхронного вызова + const usesUniversal = formula.formula && formula.formula.includes("univ("); + const usesReports = formula.formula && formula.formula.includes("report("); + + // Вызываем расчет формулы согласно установленному порядку + if (usesUniversal || usesReports) { + // Для асинхронных вызовов используем await в асинхронной IIFE + (async () => { + try { + await calculate_table_row_field(frm, tableName, rowIdx, formula.field_name, formula.formula, "refresh"); + } catch (asyncError) { + } + })(); + } else { + // Для синхронных вызовов выполняем напрямую + calculate_table_row_field(frm, tableName, rowIdx, formula.field_name, formula.formula, "refresh"); + } + } catch (error) { + } + } + }); + }); + } catch (error) { + } + }`; + + functions.push(initFunction); + + // ИСПРАВЛЕННАЯ: Создаем функцию для расчета значения поля строки таблицы + let calculateFunction = ` +// Функция для расчета значения поля в строке табличной части +async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, formula, triggerField) { + try { + + if (!frm.doc[tableName] || !frm.doc[tableName][rowIndex]) { + return; + } + + const row = frm.doc[tableName][rowIndex]; + const cdt = frm.fields_dict[tableName].grid.doctype; + const cdn = row.name; + + // === Обработка report() === + let processedFormula = formula; + if (formula.includes("report(")) { + // Регулярное выражение для поиска вызовов report() + const reportRegex = /report\\\\(([^)]+)\\\\)/g; + let match; + const reportPromises = []; + const reportPlaceholders = []; + + // Находим все вызовы report() и заменяем их на плейсхолдеры + while ((match = reportRegex.exec(formula)) !== null) { + const fullMatch = match[0]; // report(fieldName) + const fieldName = match[1]; // fieldName + + // Создаем уникальный плейсхолдер + const placeholder = \`__REPORT_PLACEHOLDER_\${reportPromises.length}__\`; + + // Ищем данные отчета в хранилище или используем предопределенный отчет + let reportName, reportFilters, reportField, reportAggregation; + + if (fieldName === "testt") { + reportName = "General Ledger"; + reportFilters = {"company":"$DefaultCompany","from_date":"$AyIlSpecial","to_date":"$AyIlSpecial"}; + reportField = "debit"; + reportAggregation = "sum"; + } else { + // Ищем в хранилище полей отчетов + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + const reportObj = Object.values(reportFields).find(f => f.name === fieldName); + + if (reportObj) { + reportName = reportObj.report; + reportFilters = JSON.parse(reportObj.filters_json || "{}"); + reportField = reportObj.field_name; + reportAggregation = reportObj.aggregation || 'sum'; + } else { + processedFormula = processedFormula.replace(fullMatch, "0"); + continue; + } + } + + // Добавляем промис запроса к отчету + reportPromises.push( + getReportFieldValue( + frm, + reportName, + reportFilters, + reportField, + reportAggregation + ) + ); + + // Запоминаем соответствие плейсхолдера и вызова + reportPlaceholders.push({ + placeholder: placeholder, + original: fullMatch + }); + + // Заменяем вызов отчета на плейсхолдер + processedFormula = processedFormula.replace(fullMatch, placeholder); + } + + // Если нашли вызовы отчетов, ждем выполнения всех запросов + if (reportPromises.length > 0) { + const reportResults = await Promise.all(reportPromises); + + // Заменяем плейсхолдеры на результаты + for (let i = 0; i < reportPlaceholders.length; i++) { + // Убедимся, что результат - числовое значение + const reportValue = reportResults[i] !== undefined ? Number(reportResults[i]) : 0; + processedFormula = processedFormula.replace( + reportPlaceholders[i].placeholder, + reportValue + ); + } + } + } + + // === ИСПРАВЛЕНИЕ: Используем transform_expression для обработки всех функций включая table() === + let finalFormula = processedFormula; + + try { + // Сначала применяем transform_expression для обработки table() и других вызовов + const transformResult = transform_expression(processedFormula, true, tableName); + if (transformResult && transformResult.transformed) { + finalFormula = transformResult.transformed; + } + } catch (transformError) { + console.error('Ошибка transform_expression:', transformError); + } + + // Вычисление выражения с доступом к объекту row + let result = 0; + try { + // Создаем функцию с доступом к контексту row и frm + const evalFunc = new Function('row', 'frm', \`return \${finalFormula};\`); + result = evalFunc(row, frm); + } catch (evalError) { + console.error('Ошибка вычисления формулы:', evalError, 'Formula:', finalFormula); + } + + if (isNaN(result)) { + result = 0; + } + + // Устанавливаем вычисленное значение + frappe.model.set_value(cdt, cdn, fieldName, result); + + // Обновляем таблицу + frm.refresh_field(tableName); + } catch (err) { + console.error('Ошибка в calculate_table_row_field:', err); + } +}`; + + + functions.push(calculateFunction); + + // Функция для получения индекса строки таблицы + let getRowIndexFunction = ` +// Функция для получения индекса строки по её имени +function get_row_index(frm, tableName, rowName) { + + // Проверяем наличие таблицы + if (!frm.doc[tableName]) { + return -1; + } + + // Ищем индекс строки по имени + for (let i = 0; i < frm.doc[tableName].length; i++) { + if (frm.doc[tableName][i].name === rowName) { + return i; + } + } + return -1; +}`; + + functions.push(getRowIndexFunction); + + return functions; +} + +function processTableRowFormulas(table_row_formulas, table_doctypes) { + // Хранилище для объединенных обработчиков + const triggerHandlers = {}; + + // Проверяем, что объект table_row_formulas не пустой + if (!table_row_formulas || Object.keys(table_row_formulas).length === 0) { + return triggerHandlers; + } + + // Получаем отсортированные формулы по порядку выполнения + const sortedFormulas = Object.entries(table_row_formulas).map(([id, formula]) => { + return { id, ...formula }; + }).sort((a, b) => { + // Сортировка по значению поля order (если порядок не задан, используем 10 по умолчанию) + const orderA = a.order !== undefined ? parseInt(a.order) : 10; + const orderB = b.order !== undefined ? parseInt(b.order) : 10; + return orderA - orderB; + }); + + // Группируем формулы по триггерам + sortedFormulas.forEach(formula => { + const tableName = formula.table_name; + const rowIndex = parseInt(formula.row_index); + const usesUniversal = formula.formula && formula.formula.includes("univ("); + const usesReports = formula.formula && formula.formula.includes("report("); + + // Если есть триггеры, добавляем их + if (formula.triggers) { + const triggers = formula.triggers.split(',').map(t => t.trim()); + + triggers.forEach(trigger => { + // Пропускаем триггер "При открытии документа", он обрабатывается отдельно + if (!trigger || trigger === "При открытии документа" || + trigger === "when_document_opens" || trigger === "document_opens") { + return; + } + + // Инициализируем обработчик триггера, если его еще нет + if (!triggerHandlers[trigger]) { + triggerHandlers[trigger] = { + isAsync: usesUniversal || usesReports, + handlers: [] + }; + } else if (usesUniversal || usesReports) { + // Если текущая формула требует async, помечаем весь обработчик как async + triggerHandlers[trigger].isAsync = true; + } + + // Код для вычисления формулы, с учетом порядка выполнения + const handlerCode = ` + // Формула строки ${rowIndex} в таблице ${tableName} (порядок: ${formula.order || 10}) + if (frm.doc.${tableName} && frm.doc.${tableName}.length >= ${rowIndex}) { + try { + // Индекс строки на 1 меньше номера строки + const rowIndex = ${rowIndex - 1}; + const formula = "${formula.formula}"; + + ${usesUniversal || usesReports ? 'await ' : ''}calculate_table_row_field(frm, "${tableName}", rowIndex, "${formula.field_name}", formula, "${trigger}"); + } catch (error) { + } + }`; + + triggerHandlers[trigger].handlers.push(handlerCode); + }); + } + }); + + // Добавляем обработчики для событий add_row и remove_row для каждой таблицы + const tables = new Set(); + sortedFormulas.forEach(formula => { + tables.add(formula.table_name); + }); + + tables.forEach(tableName => { + // Add handler + triggerHandlers[`${tableName}_add`] = { + isAsync: false, + handlers: [ + `// Обновляем индексы строк после добавления новой строки`, + `setTimeout(() => initialize_table_row_formulas(frm), 100);` + ] + }; + + // Remove handler + triggerHandlers[`${tableName}_remove`] = { + isAsync: false, + handlers: [ + `// Обновляем индексы строк после удаления строки`, + `setTimeout(() => initialize_table_row_formulas(frm), 100);` + ] + }; + }); + + return triggerHandlers; +} \ No newline at end of file diff --git a/formula_editor/public/js/formula_editor_triggers.js b/formula_editor/public/js/formula_editor_triggers.js new file mode 100644 index 0000000..026e5e9 --- /dev/null +++ b/formula_editor/public/js/formula_editor_triggers.js @@ -0,0 +1,418 @@ +function parseAndRenderTriggers($container, triggers) { + if (!triggers) return; + + // Разбиваем строку с триггерами по запятым + const triggerFields = triggers.split(',').map(field => field.trim()).filter(field => field); + + // Создаем токены для каждого поля + triggerFields.forEach(field => { + // Определяем тип триггера + let fieldType = 'field'; + + if (isPredefinedEvent(field)) { + fieldType = 'event'; + } else if (field.startsWith('univ(')) { + fieldType = 'universal'; + } else if (field.includes('_') && !field.includes('_row_')) { + // Проверяем, является ли это полем табличной части + const parts = field.split('_'); + if (parts.length >= 2) { + fieldType = 'table'; + } + } else if (field.includes('_row_')) { + fieldType = 'table-row'; + } + + addTokenToTriggers(field, $container, fieldType); + }); +} + +function getTriggerFieldsText($container) { + // Собираем значения всех токенов + const triggerFields = []; + + $container.find('.trigger-token').each(function() { + triggerFields.push($(this).data('value')); + }); + + // Объединяем их через запятую + return triggerFields.join(', '); +} + +function addTokenToTriggers(fieldname, $resultContainer, fieldType = 'field') { + // Проверяем, не добавлено ли уже это поле + const existingTokens = $resultContainer.find(`.trigger-token[data-value="${fieldname}"]`); + if (existingTokens.length > 0) { + return; // Если поле уже добавлено, не добавляем его снова + } + + // Определяем класс токена в зависимости от типа поля + let tokenClass = "trigger-token field"; + + switch (fieldType) { + case 'event': + tokenClass = "trigger-token field highlight-event"; + break; + case 'universal': + tokenClass = "trigger-token field universal-field"; + break; + case 'table': + tokenClass = "trigger-token field table-field"; + break; + case 'table-row': + tokenClass = "trigger-token field table-row-field"; + break; + default: + tokenClass = "trigger-token field"; + } + + const $token = $(`${fieldname}×`); + $resultContainer.append($token).append(' '); + + // Переносим курсор в конец контейнера + setEndOfContentEditable($resultContainer[0]); + + // Добавляем обработчики для токенов + attachTriggerTokenEvents($resultContainer); +} + +function attachTriggerTokenEvents($container) { + // Удаление токена + $container.find('.token-remove').off('click').on('click', function(e) { + e.stopPropagation(); + $(this).parent().remove(); + }); + + // Перетаскивание токенов + $container.find('.trigger-token').each(function() { + $(this).attr('draggable', 'true'); + + $(this).off('dragstart').on('dragstart', function(e) { + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'trigger-token', + value: $(this).data('value'), + html: $(this)[0].outerHTML, + tokenClass: $(this).attr('class') + })); + window.draggedTriggerElement = this; + }); + + $(this).off('dragend').on('dragend', function() { + window.draggedTriggerElement = null; + }); + }); +} + +function createTriggerToken(fieldname, fieldType) { + // Определяем класс токена в зависимости от типа поля + let tokenClass = "trigger-token field"; + + switch (fieldType) { + case 'event': + tokenClass = "trigger-token field highlight-event"; + break; + case 'universal': + tokenClass = "trigger-token field universal-field"; + break; + case 'table': + tokenClass = "trigger-token field table-field"; + break; + case 'table-row': + tokenClass = "trigger-token field table-row-field"; + break; + default: + tokenClass = "trigger-token field"; + } + + return $(`${fieldname}×`); +} + +function initTriggerTokenEvents($token) { + // Удаление токена + $token.find('.token-remove').on('click', function(e) { + e.stopPropagation(); + $(this).parent().remove(); + }); + + // Перетаскивание токена + $token.attr('draggable', 'true'); + + $token.off('dragstart').on('dragstart', function(e) { + window.draggedTriggerElement = this; + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'trigger-token', + value: $(this).data('value'), + html: $(this)[0].outerHTML, + tokenClass: $(this).attr('class') + })); + + $(this).addClass('dragging'); + }); + + $token.off('dragend').on('dragend', function() { + $(this).removeClass('dragging'); + window.draggedTriggerElement = null; + }); +} + +function isPredefinedEvent(trigger) { + const predefinedEvents = [ + "При открытии документа", + "when_document_opens", + "document_opens" + ]; + + return predefinedEvents.includes(trigger); +} + +function openTriggerFieldsEditor(frm, targetFieldname, currentTriggers, fields, grouped_fields) { + const $modal = $("#trigger_fields_modal"); + const $resultContainer = $modal.find(".trigger-result"); + + // Очистка предыдущих обработчиков и данных + $modal.off('click', '.trigger-field-btn'); + $modal.off('click', '.select-table-row-trigger-field'); + $modal.find('.apply-triggers').off('click'); + $resultContainer.empty(); + $modal.find(".trigger-field-groups-container").empty(); + + // Заполняем текущие триггеры, если они есть + if (currentTriggers) { + parseAndRenderTriggers($resultContainer, currentTriggers); + } + + // Заполняем панель с полями + populateTriggerFieldsPanel($modal, frm, grouped_fields); + + // Сохраняем данные о текущем поле и сбрасываем флаг table-row + $modal.data("target-field", targetFieldname); + $modal.data("is-for-table-row", false); + + // Обработчик для кнопки применения триггеров + $modal.find('.apply-triggers').off('click').on('click', function() { + const triggers = getTriggerFieldsText($resultContainer); + + // Проверяем, для контейнера строки таблицы или обычного поля + if ($modal.data("is-for-table-row")) { + const $tableRowContainer = $modal.data("table-row-container"); + + // Очищаем контейнер строки таблицы + $tableRowContainer.empty(); + + // Переносим триггеры из модального окна в контейнер строки таблицы + if (triggers) { + parseAndRenderTriggers($tableRowContainer, triggers); + } + } else { + // Стандартное поведение для обычного поля + const $targetInput = $(frm.wrapper).find(`.trigger-input[data-fieldname="${targetFieldname}_triggers"]`); + $targetInput.val(triggers); + } + + $modal.modal('hide'); + }); + + // Обработчики для кнопок полей + $modal.off('click', '.trigger-field-btn').on('click', '.trigger-field-btn', function() { + const fieldname = $(this).data('fieldname'); + + // Определяем тип поля по классу кнопки + let fieldType = 'field'; + if ($(this).hasClass('highlight-event')) { + fieldType = 'event'; + } else if ($(this).hasClass('universal-field-btn')) { + fieldType = 'universal'; + } else if ($(this).hasClass('table-field-btn')) { + fieldType = 'table'; + } + + addTokenToTriggers(fieldname, $resultContainer, fieldType); + }); + + // Обработчик для кнопки выбора полей строк табличных частей + $modal.off('click', '.select-table-row-trigger-field').on('click', '.select-table-row-trigger-field', function() { + openTableRowFieldSelector(frm, $resultContainer, true); + }); + + // Показываем модальное окно + $modal.modal('show'); + + // Инициализируем перетаскивание + initTriggerDragAndDrop($modal, $resultContainer); + + // Настраиваем редактируемый контейнер + setupTriggerEditableContainer($resultContainer); + + // Фокусируемся на редактируемом поле + $resultContainer.focus(); +} + +function setupTriggerEditableContainer($container) { + // Обработчик нажатия клавиш в контейнере триггеров + $container.off('keydown').on('keydown', function(e) { + // Если нажата клавиша Delete или Backspace + if (e.keyCode === 8 || e.keyCode === 46) { + const selection = window.getSelection(); + + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + + if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) { + // Текстовый узел - обычное удаление + return true; + } else { + // Возможно, выделен токен + const $selectedNode = $(range.commonAncestorContainer); + + if ($selectedNode.hasClass('trigger-token') || $selectedNode.parents('.trigger-token').length) { + // Удаляем токен + const $token = $selectedNode.hasClass('trigger-token') ? + $selectedNode : $selectedNode.parents('.trigger-token'); + $token.remove(); + e.preventDefault(); + return false; + } + } + } + } + + return true; + }); + +// Обработчик вставки текста + $container.off('paste').on('paste', function(e) { + e.preventDefault(); + + // Получаем вставляемый текст + let text = ''; + if (e.clipboardData || e.originalEvent.clipboardData) { + text = (e.clipboardData || e.originalEvent.clipboardData).getData('text/plain'); + } else if (window.clipboardData) { + text = window.clipboardData.getData('Text'); + } + + // Разбиваем вставляемый текст по запятым и добавляем как токены + if (text) { + const fields = text.split(',').map(field => field.trim()).filter(field => field); + + fields.forEach(field => { + // Определяем тип триггера + let fieldType = 'field'; + + if (isPredefinedEvent(field)) { + fieldType = 'event'; + } else if (field.startsWith('univ(')) { + fieldType = 'universal'; + } else if (field.includes('_') && !field.includes('_row_')) { + // Проверяем, является ли это полем табличной части + const parts = field.split('_'); + if (parts.length >= 2) { + fieldType = 'table'; + } + } else if (field.includes('_row_')) { + fieldType = 'table-row'; + } + + // Проверяем, не добавлено ли уже это поле + const existingTokens = $container.find(`.trigger-token[data-value="${field}"]`); + if (existingTokens.length === 0) { + addTokenToTriggers(field, $container, fieldType); + } + }); + } + }); +} + +function openTriggerFieldsEditorForTableRow(frm, $triggerContainer) { + const $modal = $("#trigger_fields_modal"); + + // Очистка предыдущих обработчиков и данных + $modal.off('click', '.trigger-field-btn'); + $modal.off('click', '.select-table-row-trigger-field'); + $modal.find('.apply-triggers').off('click'); + $modal.find(".trigger-result").empty(); + $modal.find(".trigger-field-groups-container").empty(); + + // Копируем текущие триггеры из контейнера строки таблицы + const currentTriggers = getTriggerFieldsText($triggerContainer); + + // Заполняем текущие триггеры в модальное окно триггеров + if (currentTriggers) { + parseAndRenderTriggers($modal.find(".trigger-result"), currentTriggers); + } + + // Заполняем панель с полями + populateTriggerFieldsPanel($modal, frm, frm.grouped_trigger_fields || {}); + + // Помечаем, что это для контейнера строки таблицы + $modal.data("is-for-table-row", true); + $modal.data("table-row-container", $triggerContainer); + + // Обработчик для кнопки применения триггеров + $modal.find('.apply-triggers').off('click').on('click', function() { + const triggers = getTriggerFieldsText($modal.find(".trigger-result")); + + // Проверяем, для контейнера строки таблицы или обычного поля + if ($modal.data("is-for-table-row")) { + // Очищаем контейнер строки таблицы + $triggerContainer.empty(); + + // Переносим триггеры из модального окна в контейнер строки таблицы + if (triggers) { + parseAndRenderTriggers($triggerContainer, triggers); + } + } else { + // Стандартное поведение для обычного поля + const targetFieldname = $modal.data("target-field"); + const $targetInput = $(frm.wrapper).find(`.trigger-input[data-fieldname="${targetFieldname}_triggers"]`); + $targetInput.val(triggers); + } + + $modal.modal('hide'); + }); + + // Обработчики для кнопок полей + $modal.off('click', '.trigger-field-btn').on('click', '.trigger-field-btn', function() { + const fieldname = $(this).data('fieldname'); + + // Определяем тип поля по классу кнопки + let fieldType = 'field'; + if ($(this).hasClass('highlight-event')) { + fieldType = 'event'; + } else if ($(this).hasClass('universal-field-btn')) { + fieldType = 'universal'; + } else if ($(this).hasClass('table-field-btn')) { + fieldType = 'table'; + } + + addTokenToTriggers(fieldname, $modal.find(".trigger-result"), fieldType); + }); + + // Обработчик для кнопки выбора поля строки табличной части + $modal.off('click', '.select-table-row-trigger-field').on('click', '.select-table-row-trigger-field', function() { + openTableRowFieldSelector(frm, $modal.find(".trigger-result"), true); + }); + + // Увеличиваем z-index модального окна, чтобы оно было поверх других + $modal.css('z-index', 1100); + + // Настраиваем фон + $('.modal-backdrop').not(':first').css({ + 'z-index': 1050, + 'opacity': 0.7 + }); + + // Показываем модальное окно + $modal.modal('show'); + + // Инициализируем перетаскивание + initTriggerDragAndDrop($modal, $modal.find(".trigger-result")); + + // Настраиваем редактируемый контейнер + setupTriggerEditableContainer($modal.find(".trigger-result")); + + // Фокусируемся на редактируемом поле + $modal.find(".trigger-result").focus(); +} + diff --git a/formula_editor/public/js/formula_editor_ui.js b/formula_editor/public/js/formula_editor_ui.js new file mode 100644 index 0000000..945e603 --- /dev/null +++ b/formula_editor/public/js/formula_editor_ui.js @@ -0,0 +1,612 @@ +function addTokenStyles() { + if ($("#token-styles").length) return; + + const tokenStyles = ` + `; + + $('head').append(tokenStyles); +} + +function createFormulaFieldHtml(field, dataAttr) { + return $('
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '' + + '
' + + '
' + + '
' + + '
'); +} + +function attachFormulaBuilderEvents(frm, fields, grouped_fields) { + // Сделаем поля ввода формул редактируемыми + $(frm.wrapper).find('.formula-input').prop('readonly', false); + + // Сначала удаляем существующие обработчики событий + $(frm.wrapper).off('click', '.open-formula-builder'); + + // Обработчик для кнопок открытия визуального редактора + $(frm.wrapper).on('click', '.open-formula-builder', function() { + const fieldname = $(this).data('fieldname'); + const isTableField = $(this).data('is-table') === true; + const currentFormula = $(frm.wrapper).find(`.formula-input[data-fieldname="${fieldname}"]`).val(); + + openFormulaBuilder(frm, fieldname, isTableField, currentFormula, fields, grouped_fields); + return false; + }); +} + +function attachTriggerEditorEvents(frm, fields, grouped_fields) { + // Сначала удаляем существующие обработчики событий + $(frm.wrapper).off('click', '.open-trigger-editor'); + + // Обработчик для кнопок открытия редактора полей изменения + $(frm.wrapper).on('click', '.open-trigger-editor', function() { + const fieldname = $(this).data('fieldname'); + const currentTriggers = $(frm.wrapper).find(`.trigger-input[data-fieldname="${fieldname}_triggers"]`).val(); + + openTriggerFieldsEditor(frm, fieldname, currentTriggers, fields, grouped_fields); + return false; + }); +} + +function addUniversalFieldsSection(wrapper) { + const universalSection = $('
'); + universalSection.append('

Универсальные поля

'); + universalSection.append('
'); + wrapper.append(universalSection); +} + +function addReportFieldsSection(wrapper) { + const reportSection = $('
'); + reportSection.append('

Поля отчетов

'); + reportSection.append('
'); + wrapper.append(reportSection); +} + +function addTableRowFormulasSection(wrapper) { + const tableRowSection = $('
'); + tableRowSection.append('

Формулы для строк т.ч.

'); + tableRowSection.append('
'); + wrapper.append(tableRowSection); +} + +function populateFieldsPanel($modal, frm, grouped_fields, isTableField) { + const $container = $modal.find(".field-groups-container"); + $container.empty(); + + // Добавляем секцию предопределенных событий + const $predefinedEventsGroup = $(` +
+
Предопределенные события
+
+
+ `); + + const $predefinedButtonsContainer = $predefinedEventsGroup.find('.field-buttons-container'); + + // Добавляем кнопки для предопределенных событий + const predefinedEvents = [ + { name: "При открытии документа", id: "При открытии документа" } + ]; + + predefinedEvents.forEach(event => { + const $eventBtn = $(` + + `); + + $predefinedButtonsContainer.append($eventBtn); + }); + + // Добавляем группу предопределенных событий в начало контейнера + $container.prepend($predefinedEventsGroup); + + // Добавляем стандартные поля + Object.keys(grouped_fields).forEach(group => { + const $fieldGroup = $(` +
+
${group}
+
+
+ `); + + const $buttonsContainer = $fieldGroup.find('.field-buttons-container'); + + grouped_fields[group].forEach(field => { + // Определяем, является ли это поле таблицы или нет + const isTableGroupField = group !== "Основные поля"; + + // Создаем кнопку поля с правильными атрибутами + const btnClass = isTableGroupField ? "field-btn table-field-btn" : "field-btn"; + const $fieldBtn = $(` + + `); + + $buttonsContainer.append($fieldBtn); + }); + + $container.append($fieldGroup); + }); + + // Добавляем секцию универсальных полей + if (frm.doc.universal_fields_storage) { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + if (Object.keys(universalFields).length > 0) { + const $universalFieldGroup = $(` +
+
Универсальные поля
+
+
+ `); + + const $universalButtonsContainer = $universalFieldGroup.find('.field-buttons-container'); + + Object.keys(universalFields).forEach(fieldId => { + const field = universalFields[fieldId]; + + if (!field || !field.name) return; + + const $fieldBtn = $(` + + `); + + $universalButtonsContainer.append($fieldBtn); + }); + + $container.append($universalFieldGroup); + } + } + + // Добавляем секцию полей отчетов + if (frm.doc.report_fields_storage) { + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + + if (Object.keys(reportFields).length > 0) { + const $reportFieldGroup = $(` +
+
Поля отчетов
+
+
+ `); + + const $reportButtonsContainer = $reportFieldGroup.find('.field-buttons-container'); + + Object.keys(reportFields).forEach(fieldId => { + const field = reportFields[fieldId]; + + if (!field || !field.name) return; + + const $fieldBtn = $(` + + `); + + $reportButtonsContainer.append($fieldBtn); + }); + + $container.append($reportFieldGroup); + } + } + + // Добавляем группу для полей строк табличных частей + const $tableRowGroup = $(` +
+
Поля строк табличных частей
+
+ +
+
+ `); + + $container.append($tableRowGroup); + + // НОВОЕ: Добавляем группу для полей из конкретных строк табличных частей + const $tableFieldGroup = $(` +
+
Поля из строк табличных частей
+
+ +
+
+ `); + + $container.append($tableFieldGroup); + + // Добавляем обработчик для кнопки выбора поля строки табличной части + $tableRowGroup.find('.select-table-row-field').on('click', function() { + // Определяем целевой контейнер в зависимости от контекста + const targetContainer = $modal.hasClass('trigger-focus') ? + $modal.find(".trigger-result") : + $modal.find(".formula-result"); + + openTableRowFieldSelector(frm, targetContainer, $modal.hasClass('trigger-focus')); + }); + + // НОВОЕ: Добавляем обработчик для кнопки выбора поля из строки табличной части + $tableFieldGroup.find('.select-table-field').on('click', function() { + const targetContainer = $modal.find(".formula-result"); + openTableFieldSelector(frm, targetContainer, false); + }); +} + +function populateTriggerFieldsPanel($modal, frm, grouped_fields) { + const $container = $modal.find(".trigger-field-groups-container"); + $container.empty(); + + // Добавляем секцию предопределенных событий + const $predefinedEventsGroup = $(` +
+
Предопределенные события
+
+
+ `); + + const $predefinedButtonsContainer = $predefinedEventsGroup.find('.trigger-field-buttons-container'); + + // Добавляем кнопки для предопределенных событий + const predefinedEvents = [ + { name: "При открытии документа", id: "При открытии документа" } + ]; + + predefinedEvents.forEach(event => { + const $eventBtn = $(` + + `); + + $predefinedButtonsContainer.append($eventBtn); + }); + + // Добавляем группу предопределенных событий в начало контейнера + $container.prepend($predefinedEventsGroup); + + // Добавляем секцию универсальных полей, если они есть + if (frm.doc.universal_fields_storage) { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + if (Object.keys(universalFields).length > 0) { + const $universalFieldGroup = $(` +
+
Универсальные поля
+
+
+ `); + + const $universalButtonsContainer = $universalFieldGroup.find('.trigger-field-buttons-container'); + + Object.keys(universalFields).forEach(fieldId => { + const field = universalFields[fieldId]; + + if (!field || !field.name) return; + + const $fieldBtn = $(` + + `); + + $universalButtonsContainer.append($fieldBtn); + }); + + $container.append($universalFieldGroup); + } + } + + // Добавляем обычные поля + Object.keys(grouped_fields).forEach(group => { + // Пропускаем пустые группы + if (!grouped_fields[group] || grouped_fields[group].length === 0) return; + + const $fieldGroup = $(` +
+
${group}
+
+
+ `); + + const $buttonsContainer = $fieldGroup.find('.trigger-field-buttons-container'); + + grouped_fields[group].forEach(field => { + // Создаем кнопку поля с информацией о типе поля + const isTableField = group !== "Основные поля"; + const btnClass = isTableField ? "trigger-field-btn table-field-btn" : "trigger-field-btn"; + + const $fieldBtn = $(` + + `); + + $buttonsContainer.append($fieldBtn); + }); + + $container.append($fieldGroup); + }); + + // Добавляем группу для полей строк табличных частей + const $tableRowGroup = $(` +
+
Поля строк табличных частей
+
+ +
+
+ `); + + $container.append($tableRowGroup); + + // Добавляем стили для выделения различных типов триггеров + if (!$("#trigger-field-styles").length) { + $("head").append(` + + `); + } +} + +function setEndOfContentEditable(contentEditableElement) { + let range, selection; + range = document.createRange(); + range.selectNodeContents(contentEditableElement); + range.collapse(false); + selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +} \ No newline at end of file diff --git a/formula_editor/public/js/formula_editor_universal.js b/formula_editor/public/js/formula_editor_universal.js new file mode 100644 index 0000000..ab0b270 --- /dev/null +++ b/formula_editor/public/js/formula_editor_universal.js @@ -0,0 +1,1377 @@ +function initUniversalFields(frm) { + // Если хранилище универсальных полей не существует, создаем его + if (!frm.doc.universal_fields_storage) { + frm.set_value("universal_fields_storage", "{}"); + } + + // Инициализируем обработчики событий + attachUniversalFieldsEvents(frm); + + // Для использования стилей + addUniversalFieldsStyles(); +} + +function addUniversalFieldsStyles() { + if ($('#universal-fields-styles').length) return; + + const styles = ` + `; + + $('head').append(styles); +} + +function attachUniversalFieldsEvents(frm) { + // Сначала удаляем существующие обработчики событий + $(frm.wrapper).off('click', '.add-universal-field'); + $(frm.wrapper).off('click', '.edit-universal-field'); + $(frm.wrapper).off('click', '.delete-universal-field'); + + // Обработчик нажатия на кнопку "Добавить поле" + $(frm.wrapper).on('click', '.add-universal-field', function() { + openUniversalFieldModal(frm); + }); + + // Обработчик для клика по кнопке редактирования + $(frm.wrapper).on('click', '.edit-universal-field', function(e) { + e.stopPropagation(); + const fieldId = $(this).data('field-id'); + editUniversalField(frm, fieldId); + }); + + // Обработчик для клика по кнопке удаления + $(frm.wrapper).on('click', '.delete-universal-field', function(e) { + e.stopPropagation(); + const fieldId = $(this).data('field-id'); + deleteUniversalField(frm, fieldId); + }); +} + +function openUniversalFieldModal(frm, fieldId = null) { + // Закрываем предыдущее окно, если оно открыто + if ($("#universal_fields_modal").is(":visible")) { + $("#universal_fields_modal").modal('hide'); + } + + // Получаем значения для редактирования (если указан fieldId) + let fieldValues = {}; + if (fieldId) { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + fieldValues = universalFields[fieldId] || {}; + } + + // Создаем диалог + const d = new frappe.ui.Dialog({ + title: fieldId ? 'Редактирование универсального поля' : 'Создание универсального поля', + fields: [ + { + label: 'Название поля', + fieldname: 'name', + fieldtype: 'Data', + reqd: 1, + default: fieldValues.name || '' + }, + { + label: 'Тип документа', + fieldname: 'doctype', + fieldtype: 'Link', + options: 'DocType', + reqd: 1, + default: fieldValues.doctype || '', + filters: { + "istable": 0, + "issingle": 0 + }, + onchange: function() { + const selectedDoctype = d.get_value('doctype'); + + if (selectedDoctype) { + // Очищаем таблицу фильтров перед загрузкой новых полей + if (d.fields_dict.filters && d.fields_dict.filters.grid) { + d.fields_dict.filters.grid.grid_rows.forEach(row => { + row.remove(); + }); + } + + // Загружаем поля выбранного DocType + frappe.model.with_doctype(selectedDoctype, function() { + const meta = frappe.get_meta(selectedDoctype); + + if (!meta) { + frappe.msgprint({ + title: __('Ошибка'), + message: __(`DocType ${selectedDoctype} не найден`), + indicator: 'red' + }); + return; + } + + // Получаем все доступные поля, а не только числовые + const excluded_types = [ + "Tab Break", "Column Break", "Button", "Section Break", "Column Break", + "HTML", "Table", "Attach", "Attach Image", "Code", "Text Editor", "Image", + "Heading", "Fold", "Table MultiSelect" + ]; + + const availableFields = meta.fields.filter(field => + !excluded_types.includes(field.fieldtype) + ); + + if (availableFields.length === 0) { + frappe.msgprint({ + title: __('Предупреждение'), + message: __(`В DocType ${selectedDoctype} не найдено подходящих полей`), + indicator: 'orange' + }); + return; + } + + // Обновляем опции для поля fieldname + const options = availableFields.map(field => ({ + label: field.label || field.fieldname, + value: field.fieldname + })); + + d.set_df_property('fieldname', 'options', options); + + // Если есть значение поля для редактирования, выбираем его + if (fieldValues.fieldname) { + d.set_value('fieldname', fieldValues.fieldname); + } + + // Обновляем опции для поля фильтра + const filterFieldOptions = meta.fields + .filter(field => + !excluded_types.includes(field.fieldtype) + ) + .map(field => ({ + label: field.label || field.fieldname, + value: field.fieldname + })); + + // Сохраняем информацию о полях для использования в фильтрах + d.docFields = meta.fields; + window.docFields = meta.fields; + + // Обновляем опции для фильтров + if (d.fields_dict.filters && d.fields_dict.filters.grid) { + d.fields_dict.filters.grid.update_docfield_property('field', 'options', filterFieldOptions); + } + + // Если редактируем существующее поле и есть фильтры, добавляем их + if (fieldId && fieldValues.filters && fieldValues.filters.length) { + setTimeout(() => { + // Вручную добавляем каждый фильтр + fieldValues.filters.forEach(filter => { + if (!filter || !filter.field) return; + + // Добавляем строку в таблицу фильтров + d.fields_dict.filters.grid.add_new_row(); + const lastRow = d.fields_dict.filters.grid.grid_rows[d.fields_dict.filters.grid.grid_rows.length - 1]; + + // Устанавливаем значения фильтра + lastRow.doc.field = filter.field; + lastRow.doc.operator = filter.operator || '='; + lastRow.doc.value = filter.value || ''; + + // Дополнительные свойства из оригинального фильтра + if (filter.__is_special_ay_field) { + lastRow.doc.__is_special_ay_field = true; + } + + // Обновляем отображение строки + lastRow.refresh(); + + // Находим информацию о поле для настройки типа значения + const fieldInfo = d.docFields.find(f => f.fieldname === filter.field); + if (fieldInfo) { + setTimeout(() => { + handleFilterFieldChange(d, lastRow, fieldInfo); + }, 100); + } + }); + + // Обновляем всю таблицу + d.fields_dict.filters.grid.refresh(); + }, 500); + } + }); + } + } + }, + { + label: 'Поле документа', + fieldname: 'fieldname', + fieldtype: 'Select', + options: [], + reqd: 1, + default: fieldValues.fieldname || '' + }, + { + label: 'Фильтры', + fieldname: 'filters', + fieldtype: 'Table', + fields: [ + { + label: 'Поле', + fieldname: 'field', + fieldtype: 'Select', + options: [], + reqd: 1, + in_list_view: 1, + columns: 4, + change: function() { + const grid_row = this.grid_row; + const fieldname = this.get_value(); + + if (d.docFields && fieldname) { + const fieldInfo = d.docFields.find(f => f.fieldname === fieldname); + + if (fieldInfo) { + // Изменяем тип поля значения + handleFilterFieldChange(d, grid_row, fieldInfo); + } + } + } + }, + { + label: 'Оператор', + fieldname: 'operator', + fieldtype: 'Select', + options: [ + {label: '=', value: '='}, + {label: '!=', value: '!='}, + {label: '>', value: '>'}, + {label: '>=', value: '>='}, + {label: '<', value: '<'}, + {label: '<=', value: '<='}, + {label: 'Содержит', value: 'like'}, + {label: 'Не содержит', value: 'not like'}, + {label: 'В списке', value: 'in'}, + {label: 'Не в списке', value: 'not in'} + ], + default: '=', + reqd: 1, + in_list_view: 1, + columns: 3 + }, + { + label: 'Значение', + fieldname: 'value', + fieldtype: 'Data', + in_list_view: 1, + columns: 4 + } + ], + default: [] + } + ], + primary_action_label: fieldId ? 'Обновить' : 'Создать', + primary_action: function() { + const values = d.get_values(); + + // Проверяем, что все обязательные поля заполнены + if (!values.name || !values.doctype || !values.fieldname) { + frappe.msgprint({ + title: __('Внимание'), + message: __('Заполните все обязательные поля'), + indicator: 'orange' + }); + return; + } + + // Сохраняем данные + saveUniversalField(frm, values, fieldId); + + d.hide(); + } + }); + + // Безопасно инициализируем обработчики для строк фильтров + try { + if (d.fields_dict.filters && d.fields_dict.filters.grid) { + const filterField = d.fields_dict.filters.grid.get_field('field'); + if (filterField && typeof filterField.refresh === 'function') { + filterField.refresh(); + } + + // Добавляем обработчик на событие добавления строки + d.fields_dict.filters.grid.on('row-added', function(row) { + if (d.docFields) { + window.docFields = d.docFields; + } + }); + } + } catch (error) { + } + + // Если редактируем существующее поле, вызываем событие изменения doctype для загрузки полей + if (fieldId && fieldValues.doctype) { + // Даем время на инициализацию компонентов + setTimeout(() => { + d.fields_dict.doctype.df.onchange.call(d.fields_dict.doctype); + }, 200); + } + + // Показываем диалог + d.show(); +} + +function handleFilterFieldChange(dialog, grid_row, fieldInfo) { + + // Проверяем, что у нас есть все необходимые объекты + if (!dialog || !grid_row || !fieldInfo) { + return; + } + + // Получаем текущее значение + let currentValue = undefined; + try { + if (grid_row.doc) { + currentValue = grid_row.doc.value; + } + } catch (error) { + } + + // Определяем новый тип поля в зависимости от типа выбранного поля + let fieldtype = 'Data'; + let options = ''; + + switch (fieldInfo.fieldtype) { + case 'Select': + fieldtype = 'Select'; + options = fieldInfo.options; + break; + + case 'Link': + fieldtype = 'Link'; + options = fieldInfo.options; + break; + + case 'Check': + fieldtype = 'Check'; + break; + + case 'Date': + fieldtype = 'Select'; // Используем Select для показа выбора полей даты + + // Получаем target_doctype из основной формы + const target_doctype = cur_frm.doc.target_doctype; + + if (target_doctype) { + + // Загружаем поля целевого DocType + try { + frappe.model.with_doctype(target_doctype, function() { + const targetMeta = frappe.get_meta(target_doctype); + if (targetMeta) { + // Фильтруем только поля типа Date и специальные поля ay/il из целевого DocType + const targetDateFields = targetMeta.fields.filter(field => + field.fieldtype === 'Date' || + field.fieldname === 'ay' || + field.fieldname === 'il' || + field.fieldname === 'rüb' + ); + + // Проверяем, есть ли специальные поля ay и il одновременно + 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) { + // Создаем опции для полей с датой + options = targetDateFields.filter(field => field.fieldtype === 'Date').map(field => ({ + label: field.label || field.fieldname, + value: `doc.${field.fieldname}` + })); + + // Если есть оба специальных поля, добавляем опцию для выбора месяца + if (hasAyField && hasIlField) { + options.push({ + label: 'Месяц (Ay)', + value: 'doc.ay' + }); + } + + if (hasRubField && hasIlField) { + options.push({ + label: 'Квартал (Rüb)', + value: 'doc.rüb' + }); + } + + // Добавляем опцию для пользовательского ввода + options.push({ + label: 'Указать дату...', + value: 'custom' + }); + + // Обновляем свойства поля значения + if (dialog.fields_dict.filters && dialog.fields_dict.filters.grid) { + dialog.fields_dict.filters.grid.update_docfield_property('value', 'fieldtype', fieldtype); + dialog.fields_dict.filters.grid.update_docfield_property('value', 'options', options); + + // Обновляем контрол + if (grid_row && typeof grid_row.refresh === 'function') { + grid_row.refresh(); + } + } + } else { + fieldtype = 'Date'; // Если нет полей с датой, используем обычный Date + } + } else { + fieldtype = 'Date'; // Если не удалось получить метаданные, используем обычный Date + } + }); + } catch (error) { + fieldtype = 'Date'; // В случае ошибки используем обычный Date + } + } else { + fieldtype = 'Date'; // Если target_doctype не указан, используем обычный Date + } + break; + + case 'Datetime': + fieldtype = 'Datetime'; + break; + + case 'Int': + case 'Float': + case 'Currency': + fieldtype = 'Float'; + break; + + default: + fieldtype = 'Data'; + } + + try { + // Обновляем свойства поля значения (только для не-Date полей, для Date это делается в асинхронном колбэке) + if (fieldInfo.fieldtype !== 'Date' && dialog.fields_dict.filters && dialog.fields_dict.filters.grid) { + dialog.fields_dict.filters.grid.update_docfield_property('value', 'fieldtype', fieldtype); + dialog.fields_dict.filters.grid.update_docfield_property('value', 'options', options); + } + + // Обработчик для опции "Указать дату..." и опции "Месяц (Ay)" (для типа Date) + if (fieldInfo.fieldtype === 'Date' && fieldtype === 'Select') { + // Добавляем обработчик для изменения селекта поля даты + try { + // Безопасно устанавливаем обработчик onchange + if (dialog.fields_dict.filters.grid.fields_dict && dialog.fields_dict.filters.grid.fields_dict.value) { + const valueField = dialog.fields_dict.filters.grid.fields_dict.value; + if (valueField.df) { + valueField.df.onchange = function(e) { + try { + // Для случаев, когда this.get_value не существует или не работает + let value = ''; + + // Пробуем разные способы получить значение + if (this.get_value && typeof this.get_value === 'function') { + value = this.get_value(); + } else if (e && e.target) { + value = e.target.value; + } else if (grid_row && grid_row.doc) { + value = grid_row.doc.value; + } + + if (value === 'custom') { + // Переключаем на ручной ввод даты + dialog.fields_dict.filters.grid.update_docfield_property('value', 'fieldtype', 'Date'); + dialog.fields_dict.filters.grid.update_docfield_property('value', 'options', ''); + + // Обновляем контрол + if (grid_row && typeof grid_row.refresh_field === 'function') { + grid_row.refresh_field('value'); + } + } + else if (value === 'doc.ay') { + + // Для doc.ay не меняем тип поля, только запоминаем в grid_row, что это специальный случай + if (grid_row && grid_row.doc) { + grid_row.doc.__is_special_ay_field = true; + } + } + else if (value === 'doc.rüb') { + // Помечаем в data-атрибуте, что это специальное поле rüb + if (grid_row && grid_row.doc) { + grid_row.doc.__is_special_rub_field = true; + } + } + } catch (error) { + } + }; + } + } + } catch (error) { + } + } + } catch (error) { + } + + try { + // Пересоздаем контрол (только для не-Date полей, для Date это делается в асинхронном колбэке) + if (fieldInfo.fieldtype !== 'Date' && grid_row && typeof grid_row.refresh === 'function') { + grid_row.refresh(); + } + + // Устанавливаем значение обратно + if (currentValue !== undefined && grid_row && grid_row.doc) { + grid_row.doc.value = currentValue; + if (typeof grid_row.refresh_field === 'function') { + grid_row.refresh_field('value'); + } + } + } catch (error) { + } +} + +function loadDoctypeFields(doctype) { + const $modal = $("#universal_fields_modal"); + const $fieldsSelect = $modal.find('#universal_field_fieldname'); + const $filterFieldsSelect = $modal.find('.filter-field'); + + // Показываем индикатор загрузки + $fieldsSelect.html(''); + + // Получаем поля DocType + frappe.model.with_doctype(doctype, function() { + const meta = frappe.get_meta(doctype); + + if (!meta) { + frappe.msgprint({ + title: __('Ошибка'), + message: __(`DocType ${doctype} не найден`), + indicator: 'red' + }); + $fieldsSelect.html(''); + $modal.find('.universal-fields-container').hide(); + return; + } + + $fieldsSelect.empty(); + $fieldsSelect.append(''); + + $filterFieldsSelect.empty(); + $filterFieldsSelect.append(''); + + // Фильтруем поля только для числовых типов для основного поля + const numericTypes = ['Int', 'Float', 'Currency']; + + // Для фильтров разрешаем гораздо больше типов + const filterTypes = [ + 'Int', 'Float', 'Currency', 'Data', 'Text', 'Date', 'Datetime', + 'Check', 'Select', 'Link', 'Small Text', 'Read Only' + ]; + + // Сохраняем все поля для использования в фильтрах + window.docFields = meta.fields; + + let hasNumericFields = false; + + meta.fields.forEach(field => { + // Для основного поля - только числовые типы + if (field.fieldname && numericTypes.includes(field.fieldtype)) { + $fieldsSelect.append(``); + hasNumericFields = true; + } + + // Для фильтров - более широкий набор типов + if (field.fieldname && filterTypes.includes(field.fieldtype)) { + $filterFieldsSelect.append(``); + } + }); + + // Показываем контейнер полей только если найдены числовые поля + if (hasNumericFields) { + $modal.find('.universal-fields-container').show(); + } else { + $modal.find('.universal-fields-container').hide(); + frappe.msgprint({ + title: __('Предупреждение'), + message: __(`В DocType ${doctype} не найдено числовых полей`), + indicator: 'orange' + }); + } + }); +} + +function addFilterRow($modal) { + const $filters = $modal.find('.universal-filters'); + const doctype = $modal.find('#universal_field_doctype').val(); + + if (!doctype) return; + + const $newRow = $(` +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ `); + + // Копируем опции полей из первой строки фильтра + const $firstFilterSelect = $modal.find('.filter-field').first(); + $newRow.find('.filter-field').html($firstFilterSelect.html()); + + $filters.append($newRow); + + // Добавляем обработчик изменения поля фильтра + $newRow.find('.filter-field').on('change', function() { + const fieldname = $(this).val(); + const fieldInfo = getFieldInfo(fieldname); + updateFilterValueField($(this).closest('.filter-row'), fieldInfo); + }); +} + +function getFieldInfo(fieldname) { + if (!window.docFields || !fieldname) return null; + + return window.docFields.find(field => field.fieldname === fieldname); +} + +function updateFilterValueField($filterRow, fieldInfo) { + const $valueContainer = $filterRow.find('.filter-value-container'); + const $operator = $filterRow.find('.filter-operator'); + + // Если информация о поле отсутствует, возвращаем обычное текстовое поле + if (!fieldInfo) { + $valueContainer.html(''); + return; + } + + // В зависимости от типа поля создаем соответствующий элемент ввода + switch (fieldInfo.fieldtype) { + 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) { + // Фильтруем поля даты и специальные поля ay/il + const targetDateFields = targetMeta.fields.filter(field => + field.fieldtype === 'Date' || + field.fieldname === 'ay' || + field.fieldname === 'il' + ); + + // Проверяем, есть ли специальные поля ay и il одновременно + const hasAyField = targetDateFields.some(field => field.fieldname === 'ay'); + const hasIlField = targetDateFields.some(field => field.fieldname === 'il'); + + if (targetDateFields.length > 0) { + // Создаем селект с опциями из полей с датой + let selectHtml = ''; + + $valueContainer.html(selectHtml); + + // Добавляем обработчик для опции "Указать дату..." + $valueContainer.find('.filter-value').on('change', function() { + const selectedValue = $(this).val(); + + if (selectedValue === 'custom') { + $valueContainer.html(''); + } + else if (selectedValue === 'doc.ay') { + // Помечаем в data-атрибуте, что это специальное поле ay + $(this).attr('data-is-special-ay-field', 'true'); + } + }); + } else { + $valueContainer.html(''); + } + } else { + $valueContainer.html(''); + } + }); + } else { + $valueContainer.html(''); + } + break; + + case 'Datetime': + $valueContainer.html(''); + break; + + case 'Select': + if (fieldInfo.options) { + let selectHtml = ''; + $valueContainer.html(selectHtml); + } else { + $valueContainer.html(''); + } + break; + + case 'Link': + if (fieldInfo.options) { + const targetDoctype = fieldInfo.options; + $valueContainer.html(` +
+ + +
+ `); + + // Инициализация автодополнения для Link полей + initLinkAutocomplete($valueContainer.find('.filter-value')); + } else { + $valueContainer.html(''); + } + break; + + case 'Check': + $valueContainer.html(` + + `); + break; + + case 'Int': + $valueContainer.html(''); + break; + + case 'Float': + case 'Currency': + $valueContainer.html(''); + break; + + default: + $valueContainer.html(''); + } + + // Обновляем доступные операторы в зависимости от типа поля + updateOperators($operator, fieldInfo.fieldtype); +} + +function initLinkAutocomplete($input) { + const targetDoctype = $input.data('link-target'); + if (!targetDoctype) return; + + $input.on('focus', function() { + if (!$input.data('initialized')) { + $input.data('initialized', true); + + // Загружаем значения из целевого DocType + frappe.call({ + method: "frappe.desk.search.search_link", + args: { + doctype: targetDoctype, + txt: "" + }, + callback: function(r) { + if (r.results && r.results.length) { + window.linkOptions = r.results.map(item => item.value); + } + } + }); + } + }); + + $input.on('input', function() { + const searchTerm = $(this).val().toLowerCase(); + const $dropdown = $(this).siblings('.link-dropdown'); + + if (!window.linkOptions || searchTerm.length < 1) { + $dropdown.hide(); + return; + } + + // Фильтруем значения по поисковому запросу + const filteredOptions = window.linkOptions.filter( + option => option.toLowerCase().includes(searchTerm) + ); + + // Отображаем результаты в выпадающем списке + if (filteredOptions.length > 0) { + $dropdown.empty(); + filteredOptions.forEach(option => { + $dropdown.append(``); + }); + $dropdown.show(); + } else { + $dropdown.hide(); + } + }); + + // Обработчик выбора значения из выпадающего списка + $(document).on('click', '.link-item', function() { + const selectedValue = $(this).text(); + $(this).closest('.awesomplete').find('.filter-value').val(selectedValue); + $(this).closest('.link-dropdown').hide(); + }); +} + +function updateOperators($operator, fieldtype) { + $operator.empty(); + + // Базовые операторы для всех типов + $operator.append(''); + $operator.append(''); + + // Дополнительные операторы в зависимости от типа + if (['Int', 'Float', 'Currency', 'Date', 'Datetime'].includes(fieldtype)) { + $operator.append(''); + $operator.append(''); + $operator.append(''); + $operator.append(''); + } + + if (['Data', 'Text', 'Small Text', 'Link', 'Select'].includes(fieldtype)) { + $operator.append(''); + $operator.append(''); + } + + if (['Link', 'Select'].includes(fieldtype)) { + $operator.append(''); + $operator.append(''); + } +} + +function validateUniversalFieldForm($modal) { + const name = $modal.find('#universal_field_name').val().trim(); + const doctype = $modal.find('#universal_field_doctype').val(); + const fieldname = $modal.find('#universal_field_fieldname').val(); + + // Проверяем, что все обязательные поля заполнены + const isValid = name && doctype && fieldname; + $modal.find('.save-universal-field').prop('disabled', !isValid); +} + +function saveUniversalField(frm, values, fieldId = null) { + // Используем имя поля в качестве идентификатора вместо случайной строки + const newFieldId = fieldId || values.name.toLowerCase().replace(/[^a-z0-9_]/g, '_'); + + try { + let universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + // Проверяем, не существует ли уже поле с таким именем (кроме текущего редактируемого) + const existingField = Object.keys(universalFields).find(key => + key !== newFieldId && universalFields[key].name === values.name + ); + + if (existingField) { + frappe.msgprint({ + title: __('Внимание'), + message: __('Поле с таким именем уже существует.'), + indicator: 'orange' + }); + return; + } + + // Получаем тип поля + let fieldtype = ''; + if (values.doctype && values.fieldname) { + const meta = frappe.get_meta(values.doctype); + if (meta) { + const field = meta.fields.find(f => f.fieldname === values.fieldname); + if (field) { + fieldtype = field.fieldtype; + } + } + } + + // Проверяем фильтры на наличие специальных полей ay/il + if (values.filters && Array.isArray(values.filters)) { + values.filters.forEach(filter => { + if (typeof filter.value === 'string' && filter.value === 'doc.ay') { + filter.__is_special_ay_field = true; + } + }); + } + + // Сохраняем информацию о поле в идентификатор, соответствующий имени поля + universalFields[newFieldId] = { + name: values.name, + doctype: values.doctype, + fieldname: values.fieldname, + fieldtype: fieldtype, + filters: values.filters || [] + }; + + frm.set_value("universal_fields_storage", JSON.stringify(universalFields)); + + frappe.show_alert({ + message: __('Универсальное поле сохранено'), + indicator: 'green' + }); + + renderUniversalFields(frm); + } catch (error) { + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось сохранить универсальное поле.'), + indicator: 'red' + }); + } +} + +function deleteUniversalField(frm, fieldId) { + frappe.confirm( + 'Вы уверены, что хотите удалить это универсальное поле?', + function() { + try { + let universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + delete universalFields[fieldId]; + frm.set_value("universal_fields_storage", JSON.stringify(universalFields)); + + // Перерисовываем список полей + renderUniversalFields(frm); + + frappe.show_alert({ + message: __('Универсальное поле удалено'), + indicator: 'green' + }); + } catch (error) { + frappe.msgprint({ + title: __('Ошибка'), + message: __('Не удалось удалить универсальное поле.'), + indicator: 'red' + }); + } + } + ); +} + +function editUniversalField(frm, fieldId) { + openUniversalFieldModal(frm, fieldId); +} + +function renderUniversalFields(frm) { + const $container = $(frm.wrapper).find('.universal-fields-list'); + $container.empty(); + + try { + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + + if (Object.keys(universalFields).length === 0) { + $container.append('
Нет универсальных полей. Нажмите "Добавить поле" для создания.
'); + return; + } + + Object.keys(universalFields).forEach(fieldId => { + const field = universalFields[fieldId]; + + const $fieldButton = $(` +
+ +
+ + +
+
+ `); + + // Показываем/скрываем кнопки действий при наведении + $fieldButton.hover( + function() { $(this).find('.field-actions').show(); }, + function() { $(this).find('.field-actions').hide(); } + ); + + $container.append($fieldButton); + }); + + // Инициализируем drag-n-drop для универсальных полей + $container.find('.universal-field-btn').each(function() { + $(this).attr('draggable', 'true'); + + $(this).off('dragstart').on('dragstart', function(e) { + const fieldId = $(this).data('field-id'); + const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}"); + const field = universalFields[fieldId]; + + e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'universal-field', + fieldId: fieldId, + name: field.name + })); + }); + }); + } catch (error) { + $container.append('
Ошибка при загрузке универсальных полей.
'); + } +} + +function generateUniversalFieldsFunction(universal_fields) { + let universalFieldsCode = + ` + // Функция для расчета универсальных полей + async function calculate_universal_fields(frm) { + ${Object.keys(universal_fields).map(fieldId => { + const field = universal_fields[fieldId]; + if (!field || !field.name || !field.doctype || !field.fieldname) { + return ''; + } + + // Обработка фильтров для ссылок на поля документа + const processedFilters = (field.filters || []).map(filter => { + let newFilter = {...filter}; + + // Проверяем, является ли значение ссылкой на поле документа + if (typeof newFilter.value === 'string' && newFilter.value.startsWith('doc.')) { + const docField = newFilter.value.substring(4); // Убираем 'doc.' + newFilter.valueType = 'field'; + newFilter.docField = docField; + + // Проверяем, является ли это специальным полем ay, il или rüb + newFilter.__is_special_ay_field = (docField === 'ay' || docField === 'il'); + newFilter.__is_special_rub_field = (docField === 'rüb' || docField === 'il'); + } else { + newFilter.valueType = 'static'; + } + return newFilter; + }); + + return ` + // Расчет универсального поля: ${field.name} + try { + let univ_${fieldId} = 0; + // Создаем массив фильтров + let filters_${fieldId} = []; + + // Обрабатываем фильтры с учетом возможных ссылок на поля документа + let skipCalculation_${fieldId} = false; + + ${processedFilters.map(filter => { + if (filter.valueType === 'field') { + if (filter.__is_special_ay_field) { + return ` + // Фильтр со ссылкой на специальное поле месяца (ay) или года (il) + if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { + // Получаем значения месяца и года + const docValues = { + ay: frm.doc["ay"], + il: frm.doc["il"] + }; + + // Проверяем, что поле ay имеет допустимое значение + if (!docValues.ay || typeof docValues.ay !== 'string') { + skipCalculation_${fieldId} = true; + return; + } + + // Проверяем, что поле il имеет допустимое значение + if (!docValues.il || isNaN(parseInt(docValues.il))) { + skipCalculation_${fieldId} = true; + return; + } + + // Обрабатываем специальные поля ay и il для получения диапазона дат + const dateRange = processAzerbayjanDateField(docValues); + + if (dateRange) { + // Добавляем два фильтра для диапазона дат + if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === ">=")) + filters_${fieldId}.push(["${filter.field}", ">=", dateRange.start]); + if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === "<")) + filters_${fieldId}.push(["${filter.field}", "<=", dateRange.end]); + } else { + skipCalculation_${fieldId} = true; + } + } else { + skipCalculation_${fieldId} = true; + } + `; + } else if (filter.__is_special_rub_field) { + return ` + // Фильтр со ссылкой на специальное поле квартала (rüb) или года (il) + if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { + // Получаем значения квартала и года + const docValues = { + rüb: frm.doc["rüb"], + il: frm.doc["il"] + }; + + // Проверяем, что поле rüb имеет допустимое значение + if (!docValues.rüb || typeof docValues.rüb !== 'string') { + skipCalculation_${fieldId} = true; + return; + } + + // Проверяем, что поле il имеет допустимое значение + if (!docValues.il || isNaN(parseInt(docValues.il))) { + skipCalculation_${fieldId} = true; + return; + } + + // Обрабатываем специальные поля rüb и il для получения диапазона дат + const dateRange = processQuarterDateField(docValues); + + if (dateRange) { + // Добавляем два фильтра для диапазона дат + if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === ">=")) + filters_${fieldId}.push(["${filter.field}", ">=", dateRange.start]); + if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === "<")) + filters_${fieldId}.push(["${filter.field}", "<=", dateRange.end]); + } else { + skipCalculation_${fieldId} = true; + } + } else { + skipCalculation_${fieldId} = true; + } + `; + } else { + return ` + // Фильтр со ссылкой на поле документа: ${filter.docField} + if (frm.doc["${filter.docField}"] !== undefined) { + filters_${fieldId}.push(["${filter.field}", "${filter.operator}", frm.doc["${filter.docField}"]]); + } else { + skipCalculation_${fieldId} = true; + } + `; + } + } else { + // Обычные статические фильтры + return ` + // Статический фильтр + filters_${fieldId}.push(["${filter.field}", "${filter.operator}", "${filter.value}"]); + `; + } + }).join('\n ')} + + // Проверяем, нужно ли пропустить расчет + if (skipCalculation_${fieldId}) { + if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; + frm.doc.__universalFields["${fieldId}"] = 0; + return; + } + + const result_${fieldId} = await frappe.db.get_list("${field.doctype}", { + fields: ["${field.fieldname}"], + filters: filters_${fieldId}, + limit: 1000 + }); + + if (result_${fieldId} && result_${fieldId}.length > 0) { + result_${fieldId}.forEach(r => { + univ_${fieldId} += (parseFloat(r.${field.fieldname}) || 0); + }); + } + + // Сохраняем результат в временное поле документа + if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; + frm.doc.__universalFields["${fieldId}"] = univ_${fieldId}; + } catch (error) { + if (!frm.doc.__universalFields) frm.doc.__universalFields = {}; + frm.doc.__universalFields["${fieldId}"] = 0; + }`; + }).filter(code => code !== '').join('\n')} + } + `; + + // Добавляем функцию для обработки специальных полей с кварталом + universalFieldsCode += ` + // Функция для обработки квартала (rüb) и года (il) + function processQuarterDateField(docValues) { + // Проверяем, есть ли у нас значения rüb и il + if (docValues && docValues.rüb && docValues.il) { + // Получаем номер квартала (преобразуем из строки вида "1 Rüb" в число 1) + const quarterStr = docValues.rüb; + const quarterMatch = quarterStr.match(/(\\d+)/); + + if (!quarterMatch) { + return null; + } + + const quarterNum = parseInt(quarterMatch[1]); + + // Получаем год из поля il или используем текущий год + let year = docValues.il || new Date().getFullYear(); + + // Определяем начальный и конечный месяц квартала + let startMonth, endMonth; + + switch(quarterNum) { + case 1: + startMonth = 1; // Январь + endMonth = 3; // Март + break; + case 2: + startMonth = 4; // Апрель + endMonth = 6; // Июнь + break; + case 3: + startMonth = 7; // Июль + endMonth = 9; // Сентябрь + break; + case 4: + startMonth = 10; // Октябрь + endMonth = 12; // Декабрь + break; + default: + return null; + } + + // Определяем первый день квартала (1-е число начального месяца) + const firstDay = new Date(year, startMonth - 1, 1); + + // Последний день квартала (последнее число конечного месяца) + const lastDay = new Date(year, endMonth, 0); + + // Форматируем даты в формате YYYY-MM-DD + const firstDayStr = \`\${year}-\${String(startMonth).padStart(2, '0')}-01\`; + const lastDayStr = \`\${year}-\${String(endMonth).padStart(2, '0')}-\${String(lastDay.getDate()).padStart(2, '0')}\`; + + // Возвращаем объект с диапазоном дат + return { + start: firstDayStr, + end: lastDayStr + }; + } + + // Если это не специальное поле или значение не распознано, возвращаем null + return null; + } + `; + + // Добавляем функцию для обработки специальных полей с азербайджанскими месяцами + universalFieldsCode += ` + // Функция для обработки специальных полей с азербайджанскими месяцами + function processAzerbayjanDateField(docValues) { + // Словарь для сопоставления названий месяцев с их номерами + const monthNames = { + "Yanvar": 1, + "Fevral": 2, + "Mart": 3, + "Aprel": 4, + "May": 5, + "İyun": 6, + "İyul": 7, + "Avqust": 8, + "Sentyabr": 9, + "Oktyabr": 10, + "Noyabr": 11, + "Dekabr": 12 + }; + + // Проверяем наличие квартала (rüb) и года (il) + if (docValues && docValues.rüb && docValues.il) { + // Вызываем специальную функцию для обработки квартала + return processQuarterDateField(docValues); + } + + // Проверяем, есть ли у нас значения ay и il + if (docValues && docValues.ay && monthNames[docValues.ay]) { + // Получаем номер месяца + const monthNum = monthNames[docValues.ay]; + + // Получаем год из поля il или используем текущий год + let year = docValues.il || new Date().getFullYear(); + + // Определяем первый день месяца (1-е число) + const firstDay = new Date(year, monthNum - 1, 1); + + // Последний день месяца (получаем последнее число месяца) + const lastDay = new Date(year, monthNum, 0); + + // Форматируем даты в формате YYYY-MM-DD + const firstDayStr = \`\${year}-\${String(monthNum).padStart(2, '0')}-01\`; + const lastDayStr = \`\${year}-\${String(monthNum).padStart(2, '0')}-\${String(lastDay.getDate()).padStart(2, '0')}\`; + + // Возвращаем объект с диапазоном дат + return { + start: firstDayStr, + end: lastDayStr + }; + } + + // Если это не специальное поле или значение не распознано, возвращаем null + return null; + } + `; + return universalFieldsCode; +} + diff --git a/formula_editor/public/js/formula_editor_utils.js b/formula_editor/public/js/formula_editor_utils.js new file mode 100644 index 0000000..87dee09 --- /dev/null +++ b/formula_editor/public/js/formula_editor_utils.js @@ -0,0 +1,171 @@ +function getTableDoctypes(target_doctype, table_doctypes) { + // Пытаемся получить информацию о таблицах через API + + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "DocType", + name: target_doctype + }, + async: false, + callback: function(r) { + if (r.message && r.message.fields) { + r.message.fields.forEach(field => { + if (field.fieldtype === 'Table' && field.options) { + table_doctypes[field.fieldname] = field.options; + } + }); + } + } + }); + + // Если данные не получены, используем альтернативный способ через meta + if (Object.keys(table_doctypes).length === 0) { + + const meta = frappe.get_meta(target_doctype); + if (meta && meta.fields) { + meta.fields.forEach(field => { + if (field.fieldtype === 'Table' && field.options) { + table_doctypes[field.fieldname] = field.options; + } + }); + } + } + + // Проверяем результат + if (Object.keys(table_doctypes).length === 0) { + } else { + } +} + +function collectChildTables(formula_data, child_tables) { + Object.keys(formula_data).forEach(fieldname => { + let field_info = formula_data[fieldname]; + + if (field_info && field_info.is_table_field) { + // Извлекаем имя табличной части из fieldname (формат: tablename_fieldname) + let parts = fieldname.split('_'); + if (parts.length >= 2) { + let table_name = parts[0]; + if (!child_tables[table_name]) { + child_tables[table_name] = []; + } + child_tables[table_name].push(fieldname); + } + } + }); +} + +function processAzerbayjanDateField(docValues) { + // Словарь для сопоставления названий месяцев с их номерами + const monthNames = { + "Yanvar": 1, + "Fevral": 2, + "Mart": 3, + "Aprel": 4, + "May": 5, + "İyun": 6, + "İyul": 7, + "Avqust": 8, + "Sentyabr": 9, + "Oktyabr": 10, + "Noyabr": 11, + "Dekabr": 12 + }; + + // Проверяем наличие квартала (rüb) и года (il) + if (docValues && docValues.rüb && docValues.il) { + // Вызываем специальную функцию для обработки квартала + return processQuarterDateField(docValues); + } + + // Проверяем, есть ли у нас значения ay и il + if (docValues && docValues.ay && monthNames[docValues.ay]) { + // Получаем номер месяца + const monthNum = monthNames[docValues.ay]; + + // Получаем год из поля il или используем текущий год + let year = docValues.il || new Date().getFullYear(); + + // Определяем первый день месяца (1-е число) + const firstDay = new Date(year, monthNum - 1, 1); + + // Последний день месяца (получаем последнее число месяца) + const lastDay = new Date(year, monthNum, 0); + + // Форматируем даты в формате YYYY-MM-DD + const firstDayStr = `${year}-${String(monthNum).padStart(2, '0')}-01`; + const lastDayStr = `${year}-${String(monthNum).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`; + + // Возвращаем объект с диапазоном дат + return { + start: firstDayStr, + end: lastDayStr + }; + } + + // Если это не специальное поле или значение не распознано, возвращаем null + return null; +} + +function processQuarterDateField(docValues) { + // Проверяем, есть ли у нас значения rüb и il + if (docValues && docValues.rüb && docValues.il) { + // Получаем номер квартала (преобразуем из строки вида "1 Rüb" в число 1) + const quarterStr = docValues.rüb; + const quarterMatch = quarterStr.match(/(\d+)/); + + if (!quarterMatch) { + return null; + } + + const quarterNum = parseInt(quarterMatch[1]); + + // Получаем год из поля il или используем текущий год + let year = docValues.il || new Date().getFullYear(); + + // Определяем начальный и конечный месяц квартала + let startMonth, endMonth; + + switch(quarterNum) { + case 1: + startMonth = 1; // Январь + endMonth = 3; // Март + break; + case 2: + startMonth = 4; // Апрель + endMonth = 6; // Июнь + break; + case 3: + startMonth = 7; // Июль + endMonth = 9; // Сентябрь + break; + case 4: + startMonth = 10; // Октябрь + endMonth = 12; // Декабрь + break; + default: + return null; + } + + // Определяем первый день квартала (1-е число начального месяца) + const firstDay = new Date(year, startMonth - 1, 1); + + // Последний день квартала (последнее число конечного месяца) + const lastDay = new Date(year, endMonth, 0); + + // Форматируем даты в формате YYYY-MM-DD + const firstDayStr = `${year}-${String(startMonth).padStart(2, '0')}-01`; + const lastDayStr = `${year}-${String(endMonth).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`; + + // Возвращаем объект с диапазоном дат + return { + start: firstDayStr, + end: lastDayStr + }; + } + + // Если это не специальное поле или значение не распознано, возвращаем null + return null; +} + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cba3189 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1009 @@ +{ + "name": "formula_editor", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "eslint": "^9.29.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.1", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.29.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b4e44de --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "eslint": "^9.29.0" + } +} -- 2.45.2 From da3f4e75aa29698539d5116d8fc861242bc6c42f Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Wed, 2 Jul 2025 14:00:38 +0400 Subject: [PATCH 2/5] test commit --- formula_editor/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/formula_editor/hooks.py b/formula_editor/hooks.py index f3abe45..932f951 100644 --- a/formula_editor/hooks.py +++ b/formula_editor/hooks.py @@ -5,6 +5,7 @@ app_description = "Advanced formula editing capabilities for Frappe/ERPNext" app_email = "your.email@example.com" app_license = "MIT" + # Includes in # ------------------ -- 2.45.2 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 3/5] 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) {