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 = $(`
-
- `);
-
- 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 = $(`
-
- `);
-
- 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 = $(`
-
- `);
-
- 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(`${item.value}`);
- });
- $dropdown.show();
- } else {
- $dropdown.empty();
- $dropdown.append(`DocType не найден`);
- $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(`${option}`);
- });
- $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 = $(`
-
- `);
-
- 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 = $(`
+
+ `);
+
+ 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 = $(`
+
+ `);
+
+ 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(`${option}`);
+ });
+ $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) {
@@ -995,26 +644,33 @@ function renderReportFields(frm) {
});
// Инициализируем drag-n-drop для полей отчетов
- $container.find('.report-field-btn').each(function() {
- $(this).attr('draggable', 'true');
-
- $(this).off('dragstart').on('dragstart', function(e) {
- const fieldId = $(this).data('field-id');
- const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
- const field = reportFields[fieldId];
-
- e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
- type: 'report-field',
- fieldId: fieldId,
- name: field.name
- }));
- });
- });
+ initReportFieldsDragDrop(frm);
+
} catch (error) {
$container.append('
Ошибка при загрузке полей отчетов.
');
}
}
+function initReportFieldsDragDrop(frm) {
+ const $container = $(frm.wrapper).find('.report-fields-list');
+
+ $container.find('.report-field-btn').each(function() {
+ $(this).attr('draggable', 'true');
+
+ $(this).off('dragstart').on('dragstart', function(e) {
+ const fieldId = $(this).data('field-id');
+ const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
+ const field = reportFields[fieldId];
+
+ e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
+ type: 'report-field',
+ fieldId: fieldId,
+ name: field.name
+ }));
+ });
+ });
+}
+
function editReportField(frm, fieldId) {
try {
const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
@@ -1025,54 +681,8 @@ function editReportField(frm, fieldId) {
return;
}
- // Важно: создаем копию поля, чтобы не изменять оригинал в хранилище
- const fieldCopy = {...field};
-
- // Обрабатываем фильтры перед открытием диалога
- if (fieldCopy.filters_json) {
- try {
- let filters = JSON.parse(fieldCopy.filters_json);
- const processedFilters = {};
-
- // Проходим по всем фильтрам и обрабатываем специальные значения
- Object.keys(filters).forEach(key => {
- let value = filters[key];
-
- // Обработка дат для месяца (ay)
- if (key.toLowerCase().includes('date') && value === "$AyIlSpecial") {
- // Заменяем специальное значение на обычную дату для временного использования
- // Потом мы восстановим специальное значение при сохранении
- processedFilters["__original_" + key] = "$AyIlSpecial"; // Сохраняем оригинальное значение
- processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата
- }
- // Обработка дат для квартала (rüb)
- else if (key.toLowerCase().includes('date') && value === "$RubIlSpecial") {
- // Аналогично сохраняем оригинальное значение для квартала
- processedFilters["__original_" + key] = "$RubIlSpecial"; // Сохраняем оригинальное значение
- processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата
- }
- // Обработка ссылок на поля документа
- else if (typeof value === 'string' && value.startsWith("doc.") &&
- (key.toLowerCase().includes('date') || key.toLowerCase().includes('from') || key.toLowerCase().includes('to'))) {
- // Обрабатываем также ссылки на поля документа
- processedFilters["__original_" + key] = value; // Сохраняем оригинальное значение
- processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата
- }
- // Обычные значения без обработки
- else {
- processedFilters[key] = value;
- }
- });
-
- // Обновляем JSON фильтров в копии
- fieldCopy.filters_json = JSON.stringify(processedFilters);
- fieldCopy.__has_special_dates = true; // Маркер для последующего восстановления
- } catch (e) {
- }
- }
-
- // Открываем модальное окно с обработанной копией поля
- openReportFieldModal(frm, fieldId, fieldCopy);
+ // Открываем модальное окно с данными поля
+ openReportFieldModal(frm, fieldId, field);
} catch (error) {
frappe.msgprint({
title: __('Ошибка'),
@@ -1110,195 +720,181 @@ function deleteReportField(frm, fieldId) {
}
function generateReportFieldsFunction(report_fields) {
-
+ if (!report_fields || Object.keys(report_fields).length === 0) {
+ return "";
+ }
- return `
- window.notifiedAboutAyIl = false;
- window.notifiedAboutRubIl = false;
-async function getReportFieldValue(frm, reportName, filters, fieldName, aggregation = 'sum') {
+ return `
+// Функция для обработки азербайджанских дат
+function processAzerbayjanDateField(docValues) {
+ // Словарь для сопоставления названий месяцев с их номерами
+ const monthNames = {
+ "Yanvar": 1,
+ "Fevral": 2,
+ "Mart": 3,
+ "Aprel": 4,
+ "May": 5,
+ "İyun": 6,
+ "İyul": 7,
+ "Avqust": 8,
+ "Sentyabr": 9,
+ "Oktyabr": 10,
+ "Noyabr": 11,
+ "Dekabr": 12
+ };
+
+ // Проверяем наличие квартала (rüb) и года (il)
+ if (docValues && docValues.rüb && docValues.il) {
+ // Получаем номер квартала (преобразуем из строки вида "1 Rüb" в число 1)
+ const quarterStr = docValues.rüb;
+ const quarterMatch = quarterStr.match(/(\\d+)/);
+
+ if (!quarterMatch) {
+ return null;
+ }
+
+ const quarterNum = parseInt(quarterMatch[1]);
+
+ // Получаем год из поля il или используем текущий год
+ let year = docValues.il || new Date().getFullYear();
+
+ // Определяем начальный и конечный месяц квартала
+ let startMonth, endMonth;
+
+ switch(quarterNum) {
+ case 1:
+ startMonth = 1; // Январь
+ endMonth = 3; // Март
+ break;
+ case 2:
+ startMonth = 4; // Апрель
+ endMonth = 6; // Июнь
+ break;
+ case 3:
+ startMonth = 7; // Июль
+ endMonth = 9; // Сентябрь
+ break;
+ case 4:
+ startMonth = 10; // Октябрь
+ endMonth = 12; // Декабрь
+ break;
+ default:
+ return null;
+ }
+
+ // Определяем первый день квартала (1-е число начального месяца)
+ const firstDay = new Date(year, startMonth - 1, 1);
+
+ // Последний день квартала (последнее число конечного месяца)
+ const lastDay = new Date(year, endMonth, 0);
+
+ // Форматируем даты в формате YYYY-MM-DD
+ const firstDayStr = year + "-" + String(startMonth).padStart(2, '0') + "-01";
+ const lastDayStr = year + "-" + String(endMonth).padStart(2, '0') + "-" + String(lastDay.getDate()).padStart(2, '0');
+
+ // Возвращаем объект с диапазоном дат
+ return {
+ start: firstDayStr,
+ end: lastDayStr
+ };
+ }
+
+ // Проверяем, есть ли у нас значения ay и il
+ if (docValues && docValues.ay && monthNames[docValues.ay]) {
+ // Получаем номер месяца
+ const monthNum = monthNames[docValues.ay];
+
+ // Получаем год из поля il или используем текущий год
+ let year = docValues.il || new Date().getFullYear();
+
+ // Определяем первый день месяца (1-е число)
+ const firstDay = new Date(year, monthNum - 1, 1);
+
+ // Последний день месяца (получаем последнее число месяца)
+ const lastDay = new Date(year, monthNum, 0);
+
+ // Форматируем даты в формате YYYY-MM-DD
+ const firstDayStr = year + "-" + String(monthNum).padStart(2, '0') + "-01";
+ const lastDayStr = year + "-" + String(monthNum).padStart(2, '0') + "-" + String(lastDay.getDate()).padStart(2, '0');
+
+ // Возвращаем объект с диапазоном дат
+ return {
+ start: firstDayStr,
+ end: lastDayStr
+ };
+ }
+
+ // Если это не специальное поле или значение не распознано, возвращаем null
+ return null;
+}
+
+async function getReportFieldValue(frm, reportName, filters, fieldName, aggregation) {
try {
-
- // Проверка необходимости полей сразу для всех фильтров
- if (Object.values(filters).some(v => v === "$AyIlSpecial")) {
- if (!frm.doc["ay"] || !frm.doc["il"]) {
- if (!notifiedAboutAyIl) {
- frappe.msgprint({
- title: "Not enough data",
- message: "Please fill in the
ay and
il fields",
- indicator: "orange"
- });
- notifiedAboutAyIl = true;
- }
- return 0;
- }
- }
-
- // Проверка: если используется $RubIlSpecial, но rüb или il не заполнены — возвращаем 0
- if (Object.values(filters).some(v => v === "$RubIlSpecial")) {
- if (!frm.doc["rüb"] || !frm.doc["il"]) {
- if (!notifiedAboutRubIl) {
- frappe.msgprint({
- title: "Not enough data",
- message: "Please fill in the
rüb and
il fields",
- indicator: "orange"
- });
- notifiedAboutRubIl = true;
- }
- return 0;
- }
- }
-
// Обрабатываем фильтры с возможными ссылками на поля документа
const processedFilters = {};
- // Используем for...in loop для перебора свойств объекта filters
for (const key in filters) {
let value = filters[key];
- // Специальная обработка для $DefaultCompany
- if (value === "$DefaultCompany") {
- processedFilters[key] = frm.doc.company || frappe.defaults.get_user_default("Company") || "";
- continue;
- }
-
- // Специальная обработка для $AyIlSpecial
- if (value === "$AyIlSpecial") {
- // Проверяем, есть ли у документа поля ay и il
- if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) {
- // Обрабатываем специальные поля ay и il для получения диапазона дат
- const dateRange = processAzerbayjanDateField({
- ay: frm.doc["ay"],
- il: frm.doc["il"]
- });
-
- // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия
- if (dateRange) {
- if (key.includes('from_date')) {
- // Для начальной даты используем только >=
- processedFilters[key] = dateRange.start;
- } else if (key.includes('to_date')) {
- // Для конечной даты используем только <=
- processedFilters[key] = dateRange.end;
- } else {
- // Для других полей даты используем начало периода
- processedFilters[key] = dateRange.start;
- }
- continue;
- }
- }
- }
-
- // Специальная обработка для $RubIlSpecial
- if (value === "$RubIlSpecial") {
- // Проверяем, есть ли у документа поля rüb и il
- if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) {
- // Обрабатываем специальные поля rüb и il для получения диапазона дат
- const dateRange = processQuarterDateField({
- rüb: frm.doc["rüb"],
- il: frm.doc["il"]
- });
-
- // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия
- if (dateRange) {
- if (key.includes('from_date')) {
- // Для начальной даты используем только >=
- processedFilters[key] = dateRange.start;
- } else if (key.includes('to_date')) {
- // Для конечной даты используем только <=
- processedFilters[key] = dateRange.end;
- } else {
- // Для других полей даты используем начало периода
- processedFilters[key] = dateRange.start;
- }
- continue;
- }
- }
- }
-
// Проверяем, является ли значение ссылкой на поле документа
if (typeof value === 'string' && value.startsWith('doc.')) {
- const docField = value.substring(4); // Убираем 'doc.'
-
- // Проверяем, является ли это специальным полем ay или il
- if (docField === 'ay' || docField === 'il') {
- // Получаем значения ay и il из документа
- if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) {
- // Обрабатываем специальные поля ay и il для получения диапазона дат
- const dateRange = processAzerbayjanDateField({
- ay: frm.doc["ay"],
- il: frm.doc["il"]
- });
-
- // Если удалось получить диапазон дат, создаем правильные фильтры
- if (dateRange) {
- if (key.includes('from_date')) {
- // Для начальной даты используем только >=
- processedFilters[key] = dateRange.start;
- } else if (key.includes('to_date')) {
- // Для конечной даты используем только <=
- processedFilters[key] = dateRange.end;
- } else {
- // Для других полей даты используем начало периода
- processedFilters[key] = dateRange.start;
- }
- continue;
- }
- }
- }
- // Проверяем, является ли это специальным полем rüb
- else if (docField === 'rüb') {
- // Получаем значения rüb и il из документа
- if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) {
- // Обрабатываем специальные поля rüb и il для получения диапазона дат
- const dateRange = processQuarterDateField({
- rüb: frm.doc["rüb"],
- il: frm.doc["il"]
- });
-
- // Если удалось получить диапазон дат, создаем правильные фильтры
- if (dateRange) {
- if (key.includes('from_date')) {
- // Для начальной даты используем только >=
- processedFilters[key] = dateRange.start;
- } else if (key.includes('to_date')) {
- // Для конечной даты используем только <=
- processedFilters[key] = dateRange.end;
- } else {
- // Для других полей даты используем начало периода
- processedFilters[key] = dateRange.start;
- }
- continue;
- }
- }
- }
- // Для других полей документа
- else if (frm.doc[docField] !== undefined) {
- // Берем значение из документа
+ const docField = value.substring(4);
+ if (frm.doc[docField] !== undefined) {
processedFilters[key] = frm.doc[docField];
continue;
- } else {
- // Если поле не найдено, пропускаем фильтр
- continue;
}
}
- // Добавляем обработанное значение в фильтры
+ // Обработка специальных переменных
+ if (typeof value === 'string' && value.startsWith('$')) {
+ switch (value) {
+ case '$DefaultCompany':
+ // Получаем компанию по умолчанию из настроек или документа
+ processedFilters[key] = frm.doc.company || frappe.defaults.get_user_default("Company");
+ continue;
+
+ case '$AyIlSpecial':
+ // Обрабатываем специальные даты для азербайджанских месяцев/кварталов
+ const dateRange = processAzerbayjanDateField(frm.doc);
+ if (dateRange) {
+ // Для полей from_date и to_date используем соответствующие значения
+ if (key === 'from_date') {
+ processedFilters[key] = dateRange.start;
+ } else if (key === 'to_date') {
+ processedFilters[key] = dateRange.end;
+ } else {
+ // Для других полей используем начальную дату
+ processedFilters[key] = dateRange.start;
+ }
+ } else {
+ // Если не удалось получить диапазон дат, используем текущую дату
+ processedFilters[key] = frappe.datetime.get_today();
+ }
+ continue;
+
+ default:
+ // Для других $-переменных оставляем как есть
+ processedFilters[key] = value;
+ continue;
+ }
+ }
+
+ // Обычные значения
processedFilters[key] = value;
}
- // Проверяем кеш для экономии запросов
+ // Проверяем кеш
if (!window.reportCache) window.reportCache = {};
- const cacheKey = \`\${reportName}|\${JSON.stringify(processedFilters)}|\${fieldName}|\${aggregation}\`;
-
- // Если есть в кеше и кеш не устарел (5 минут)
+ const cacheKey = reportName + "|" + JSON.stringify(processedFilters) + "|" + fieldName + "|" + aggregation;
const now = new Date().getTime();
+
if (window.reportCache[cacheKey] &&
window.reportCache[cacheKey].timestamp > now - 5 * 60 * 1000) {
return window.reportCache[cacheKey].value;
}
-
- // Выполняем запрос к серверу с обработанными фильтрами
+
+ // Выполняем запрос к серверу
try {
const result = await frappe.call({
method: "formula_editor.api.run_report_for_formula",
@@ -1307,16 +903,15 @@ async function getReportFieldValue(frm, reportName, filters, fieldName, aggregat
filters: processedFilters,
field_name: fieldName
},
- freeze: false // Не блокируем интерфейс
+ freeze: false
});
- // Проверяем результат
- if (result && result.message) {
+ if (result && result.message !== undefined) {
if (result.message.error) {
+ console.error('Report error:', result.message.error);
return 0;
}
- // Получаем числовое значение
let value = parseFloat(result.message);
if (isNaN(value)) value = 0;
@@ -1325,66 +920,65 @@ async function getReportFieldValue(frm, reportName, filters, fieldName, aggregat
value: value,
timestamp: now
};
-
+
return value;
}
} catch (err) {
+ console.error('Report field error:', err);
}
return 0;
} catch (error) {
+ console.error('getReportFieldValue error:', error);
return 0;
}
}`;
}
-function updateFiltersJson(d) {
- try {
- // Собираем все значения фильтров
- const values = {};
- Object.keys(d.filter_controls || {}).forEach(fieldname => {
- const ctrl = d.filter_controls[fieldname];
- if (ctrl) {
- try {
- const value = ctrl.get_value();
- if (value !== undefined && value !== null && value !== '') {
- // Сохраняем все значения как есть
- values[fieldname] = value;
-
- // Проверка на специальные значения дат
- if ((value === "$AyIlSpecial" || value === "$RubIlSpecial" ||
- (typeof value === 'string' && value.startsWith("doc."))) &&
- (fieldname.toLowerCase().includes('date') ||
- fieldname.toLowerCase().includes('from') ||
- fieldname.toLowerCase().includes('to'))) {
-
- // Сохраняем оригинальное значение отдельно
- values[`__original_${fieldname}`] = value;
-
- // Временно заменяем значение на валидную дату для проверок
- values[fieldname] = frappe.datetime.get_today();
-
- // Отмечаем, что у нас есть специальные даты
- d.set_value('has_special_dates', 1);
- }
- }
- } catch (e) {
- frappe.show_alert({
- message: `Ошибка при получении значения фильтра ${fieldname}: ${e.message}`,
- indicator: 'red'
- });
- }
- }
- });
-
- // Обновляем JSON фильтров
- if (Object.keys(values).length > 0) {
- d.set_value('filters_json', JSON.stringify(values, null, 2));
- }
- } catch (e) {
- frappe.show_alert({
- message: `Ошибка при обновлении фильтров: ${e.message}`,
- indicator: 'red'
- });
+// Функция для применения агрегации к данным отчета
+function applyAggregation(data, fieldName, aggregationType) {
+ if (!data || data.length === 0) {
+ return 0;
}
+
+ const values = data.map(row => {
+ let value = null;
+ if (typeof row === 'object' && row[fieldName] !== undefined) {
+ value = row[fieldName];
+ }
+
+ // Преобразуем в число
+ const numValue = parseFloat(value);
+ return isNaN(numValue) ? 0 : numValue;
+ }).filter(v => v !== 0); // Убираем нулевые значения
+
+ if (values.length === 0) return 0;
+
+ switch(aggregationType) {
+ case 'sum':
+ return values.reduce((sum, val) => sum + val, 0);
+ case 'avg':
+ return values.reduce((sum, val) => sum + val, 0) / values.length;
+ case 'min':
+ return Math.min(...values);
+ case 'max':
+ return Math.max(...values);
+ case 'first':
+ return values[0] || 0;
+ case 'last':
+ return values[values.length - 1] || 0;
+ default:
+ return values.reduce((sum, val) => sum + val, 0);
+ }
+}
+
+// Экспорт функций для использования в других модулях
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = {
+ initReportFields,
+ generateReportFieldsFunction,
+ openReportFieldModal,
+ loadReportFields,
+ loadReportFilters
+ };
}
\ No newline at end of file
diff --git a/formula_editor/public/js/formula_editor_table_rows.js b/formula_editor/public/js/formula_editor_table_rows.js
index 9c821c9..f98373e 100644
--- a/formula_editor/public/js/formula_editor_table_rows.js
+++ b/formula_editor/public/js/formula_editor_table_rows.js
@@ -1169,7 +1169,7 @@ function generateTableRowFormulasHandlers(table_row_formulas, functions, target_
functions.push(initFunction);
- // ИСПРАВЛЕННАЯ: Создаем функцию для расчета значения поля строки таблицы
+ // Создаем функцию для расчета значения поля строки таблицы
let calculateFunction = `
// Функция для расчета значения поля в строке табличной части
async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, formula, triggerField) {
@@ -1183,98 +1183,60 @@ async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, fo
const cdt = frm.fields_dict[tableName].grid.doctype;
const cdn = row.name;
- // === Обработка report() ===
- let processedFormula = formula;
- if (formula.includes("report(")) {
- // Регулярное выражение для поиска вызовов report()
- const reportRegex = /report\\\\(([^)]+)\\\\)/g;
- let match;
- const reportPromises = [];
- const reportPlaceholders = [];
-
- // Находим все вызовы report() и заменяем их на плейсхолдеры
- while ((match = reportRegex.exec(formula)) !== null) {
- const fullMatch = match[0]; // report(fieldName)
- const fieldName = match[1]; // fieldName
-
- // Создаем уникальный плейсхолдер
- const placeholder = \`__REPORT_PLACEHOLDER_\${reportPromises.length}__\`;
-
- // Ищем данные отчета в хранилище или используем предопределенный отчет
- let reportName, reportFilters, reportField, reportAggregation;
-
- if (fieldName === "testt") {
- reportName = "General Ledger";
- reportFilters = {"company":"$DefaultCompany","from_date":"$AyIlSpecial","to_date":"$AyIlSpecial"};
- reportField = "debit";
- reportAggregation = "sum";
- } else {
- // Ищем в хранилище полей отчетов
- const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
- const reportObj = Object.values(reportFields).find(f => f.name === fieldName);
-
- if (reportObj) {
- reportName = reportObj.report;
- reportFilters = JSON.parse(reportObj.filters_json || "{}");
- reportField = reportObj.field_name;
- reportAggregation = reportObj.aggregation || 'sum';
- } else {
- processedFormula = processedFormula.replace(fullMatch, "0");
- continue;
- }
- }
-
- // Добавляем промис запроса к отчету
- reportPromises.push(
- getReportFieldValue(
- frm,
- reportName,
- reportFilters,
- reportField,
- reportAggregation
- )
- );
-
- // Запоминаем соответствие плейсхолдера и вызова
- reportPlaceholders.push({
- placeholder: placeholder,
- original: fullMatch
- });
-
- // Заменяем вызов отчета на плейсхолдер
- processedFormula = processedFormula.replace(fullMatch, placeholder);
- }
-
- // Если нашли вызовы отчетов, ждем выполнения всех запросов
- if (reportPromises.length > 0) {
- const reportResults = await Promise.all(reportPromises);
-
- // Заменяем плейсхолдеры на результаты
- for (let i = 0; i < reportPlaceholders.length; i++) {
- // Убедимся, что результат - числовое значение
- const reportValue = reportResults[i] !== undefined ? Number(reportResults[i]) : 0;
- processedFormula = processedFormula.replace(
- reportPlaceholders[i].placeholder,
- reportValue
- );
- }
- }
- }
-
- // === ИСПРАВЛЕНИЕ: Используем transform_expression для обработки всех функций включая table() ===
- let finalFormula = processedFormula;
+ // === ЕДИНАЯ ТОЧКА ОБРАБОТКИ ВСЕХ ФУНКЦИЙ ===
+ let finalFormula, reportCalls;
try {
- // Сначала применяем transform_expression для обработки table() и других вызовов
- const transformResult = transform_expression(processedFormula, true, tableName);
- if (transformResult && transformResult.transformed) {
+ // Используем transform_expression как единственную точку преобразования
+ const transformResult = transform_expression(formula, true, tableName);
+
+ if (transformResult) {
finalFormula = transformResult.transformed;
+ reportCalls = transformResult.reportCalls || [];
+ } else {
+ finalFormula = formula;
+ reportCalls = [];
}
} catch (transformError) {
console.error('Ошибка transform_expression:', transformError);
+ finalFormula = formula;
+ reportCalls = [];
}
- // Вычисление выражения с доступом к объекту row
+ // === ОБРАБОТКА АСИНХРОННЫХ ВЫЗОВОВ ОТЧЕТОВ ===
+ if (reportCalls && reportCalls.length > 0) {
+ try {
+ // Выполняем все асинхронные запросы к отчетам параллельно
+ const reportResults = await Promise.all(
+ reportCalls.map(call =>
+ getReportFieldValue(
+ frm,
+ call.reportName,
+ call.filters,
+ call.fieldName,
+ call.aggregation
+ )
+ )
+ );
+
+ // Заменяем плейсхолдеры на реальные значения
+ for (let i = 0; i < reportCalls.length; i++) {
+ const reportValue = reportResults[i] !== undefined ? Number(reportResults[i]) : 0;
+ finalFormula = finalFormula.replace(
+ reportCalls[i].placeholder,
+ reportValue
+ );
+ }
+ } catch (reportError) {
+ console.error('Ошибка получения данных отчетов:', reportError);
+ // В случае ошибки заменяем все плейсхолдеры на 0
+ for (const call of reportCalls) {
+ finalFormula = finalFormula.replace(call.placeholder, 0);
+ }
+ }
+ }
+
+ // === ВЫЧИСЛЕНИЕ ФИНАЛЬНОЙ ФОРМУЛЫ ===
let result = 0;
try {
// Создаем функцию с доступом к контексту row и frm
@@ -1298,7 +1260,6 @@ async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, fo
}
}`;
-
functions.push(calculateFunction);
// Функция для получения индекса строки таблицы
--
2.45.2
From e58995687822b5874365c45eb0ea9996e33a3176 Mon Sep 17 00:00:00 2001
From: Ali <010109ali@gmail.com>
Date: Mon, 7 Jul 2025 19:23:03 +0400
Subject: [PATCH 4/5] test last commit
---
formula_editor/public/js/formula_editor_reports.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/formula_editor/public/js/formula_editor_reports.js b/formula_editor/public/js/formula_editor_reports.js
index 8a3255d..73e7e76 100644
--- a/formula_editor/public/js/formula_editor_reports.js
+++ b/formula_editor/public/js/formula_editor_reports.js
@@ -981,4 +981,4 @@ if (typeof module !== 'undefined' && module.exports) {
loadReportFields,
loadReportFilters
};
-}
\ No newline at end of file
+}
--
2.45.2
From 7b811fb60788bff7e808ac83fa514eba59d10a3b Mon Sep 17 00:00:00 2001
From: Ali <010109ali@gmail.com>
Date: Wed, 16 Jul 2025 17:26:40 +0400
Subject: [PATCH 5/5] fixed bugs with reports ui and added rounding
---
formula_editor/api.py | 8 ++-
.../public/js/formula_editor_reports.js | 56 ++++++++++++++++---
formula_editor/public/node_modules | 1 +
3 files changed, 53 insertions(+), 12 deletions(-)
create mode 120000 formula_editor/public/node_modules
diff --git a/formula_editor/api.py b/formula_editor/api.py
index f3667b0..e2fbcd9 100644
--- a/formula_editor/api.py
+++ b/formula_editor/api.py
@@ -416,10 +416,11 @@ def extract_field_from_report(data, columns, field_name):
else:
value = None
- # Преобразуем в число
+ # Преобразуем в число и округляем до 2 знаков
try:
if value is not None:
- return float(value)
+ num_value = float(value)
+ return round(num_value, 2)
except (ValueError, TypeError):
pass
@@ -449,7 +450,8 @@ def extract_field_from_report(data, columns, field_name):
except (ValueError, TypeError):
pass
- return total
+ # Округляем итоговую сумму до 2 знаков
+ return round(total, 2)
return 0
diff --git a/formula_editor/public/js/formula_editor_reports.js b/formula_editor/public/js/formula_editor_reports.js
index 73e7e76..3153354 100644
--- a/formula_editor/public/js/formula_editor_reports.js
+++ b/formula_editor/public/js/formula_editor_reports.js
@@ -205,16 +205,33 @@ function loadReportFilters(d, reportName, existingFilters = null) {
},
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);
+ // Фильтруем ненужные базовые поля, которые автоматически обрабатываются
+ const excludeFields = ['company', 'from_date', 'to_date', 'fiscal_year', 'period_start_date', 'period_end_date'];
+ const filteredFilters = r.message.filter(filter =>
+ !excludeFields.includes(filter.fieldname)
+ );
- // Генерируем HTML для фильтров
- const filtersHtml = generateUniversalFiltersHtml(r.message, existingFilters);
- d.fields_dict.filters_container.$wrapper.html(filtersHtml);
-
- // Инициализируем контролы фильтров
- initUniversalFilterControls(d, r.message, existingFilters);
+ if (filteredFilters.length > 0) {
+ // Показываем секцию фильтров
+ d.set_df_property('filters_section', 'hidden', 0);
+ d.set_df_property('filters_helper', 'hidden', 0);
+
+ // Генерируем HTML для фильтров
+ const filtersHtml = generateUniversalFiltersHtml(filteredFilters, existingFilters);
+ d.fields_dict.filters_container.$wrapper.html(filtersHtml);
+
+ // Инициализируем контролы фильтров
+ initUniversalFilterControls(d, filteredFilters, existingFilters);
+ } else {
+ // Если после фильтрации не осталось полей, скрываем секцию
+ clearFiltersSection(d);
+
+ // Показываем информационное сообщение
+ d.fields_dict.filters_container.$wrapper.html(
+ '
'
+ );
+ d.set_df_property('filters_section', 'hidden', 0);
+ }
} else {
clearFiltersSection(d);
}
@@ -516,6 +533,15 @@ function createFallbackControl($container, filter, currentValue, d) {
function collectFiltersFromControls(d) {
const values = {};
+ // Автоматически добавляем базовые фильтры с их специальными значениями
+ values.company = '$DefaultCompany';
+ values.from_date = '$AyIlSpecial';
+ values.to_date = '$AyIlSpecial';
+
+ // Добавляем фискальный год, если нужен
+ values.fiscal_year = '$DefaultFiscalYear';
+
+ // Добавляем значения от пользовательских контролов
if (d.filter_controls) {
Object.keys(d.filter_controls).forEach(fieldname => {
const ctrl = d.filter_controls[fieldname];
@@ -539,6 +565,15 @@ function updateFiltersJson(d) {
try {
const filtersJson = collectFiltersFromControls(d);
d.set_value('filters_json', filtersJson);
+
+ // Также обновляем поля отчета при изменении фильтров
+ const reportName = d.get_value('report');
+ if (reportName) {
+ // Небольшая задержка для применения изменений
+ setTimeout(() => {
+ loadReportFields(d, reportName);
+ }, 300);
+ }
} catch (e) {
console.error('Error updating filters JSON:', e);
}
@@ -914,6 +949,9 @@ async function getReportFieldValue(frm, reportName, filters, fieldName, aggregat
let value = parseFloat(result.message);
if (isNaN(value)) value = 0;
+
+ // Округляем до 2 знаков после запятой
+ value = Math.round(value * 100) / 100;
// Сохраняем в кеш
window.reportCache[cacheKey] = {
diff --git a/formula_editor/public/node_modules b/formula_editor/public/node_modules
new file mode 120000
index 0000000..bc426b4
--- /dev/null
+++ b/formula_editor/public/node_modules
@@ -0,0 +1 @@
+/home/frappe/frappe-bench/apps/formula_editor/node_modules
\ No newline at end of file
--
2.45.2