Fixed bug with reports not working in formulas

This commit is contained in:
Ali 2025-07-07 17:33:36 +04:00
parent da3f4e75aa
commit 9fd6393388
5 changed files with 1570 additions and 1987 deletions

View File

@ -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;
}}

View File

@ -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

View File

@ -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);
// Функция для получения индекса строки таблицы