Fixed bug with reports not working in formulas
This commit is contained in:
parent
da3f4e75aa
commit
9fd6393388
|
|
@ -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()
|
||||
|
||||
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")
|
||||
|
||||
# Удаляем найденные скрипты
|
||||
for script in client_scripts:
|
||||
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
|
||||
}
|
||||
]
|
||||
fields=["name"]
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
# Удаляем найденные скрипты
|
||||
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 <b>ay</b> and <b>il</b> 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 <b>rüb</b> and <b>il</b> 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;
|
||||
}}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||
|
||||
// Функция для получения индекса строки таблицы
|
||||
|
|
|
|||
Loading…
Reference in New Issue