Added report support, but have a lot of bugs
This commit is contained in:
parent
1680da6f31
commit
30f878fc82
|
|
@ -1,4 +1,10 @@
|
|||
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
|
||||
import re
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctype_fields(doctype):
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
|
@ -77,4 +83,705 @@ def on_trash(doc, method=None):
|
|||
frappe.delete_doc("Client Script", script.name, force=True)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.log_error(f"Не удалось удалить клиентский скрипт {script.name}: {str(e)}")
|
||||
frappe.log_error(f"Не удалось удалить клиентский скрипт {script.name}: {str(e)}")
|
||||
|
||||
@frappe.whitelist()
|
||||
def run_report_for_formula(report_name, filters, field_name=None):
|
||||
"""Выполняет отчет с указанными фильтрами и возвращает результат для формулы"""
|
||||
try:
|
||||
frappe.log_error(f"ОТЧЕТ API - начало запроса: {report_name}, поле: {field_name}", "Report API Debug")
|
||||
frappe.log_error(f"ОТЧЕТ API - фильтры: {filters}", "Report API Debug")
|
||||
|
||||
# Преобразуем фильтры, если они пришли в виде строки
|
||||
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")
|
||||
frappe.log_error(f"ОТЧЕТ API - установлена компания по умолчанию: {final_filters['company']}", "Report API Debug")
|
||||
|
||||
# Находим специальные операторы для дат
|
||||
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
|
||||
frappe.log_error(f"ОТЧЕТ API - используем from_date из {from_date_key}: {final_filters['from_date']}", "Report API Debug")
|
||||
|
||||
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
|
||||
frappe.log_error(f"ОТЧЕТ API - используем to_date из {to_date_key}: {final_filters['to_date']}", "Report API Debug")
|
||||
|
||||
# Если нет специальных операторов, проверяем обычные поля from_date и to_date
|
||||
if 'from_date' not in final_filters and 'from_date' in filters:
|
||||
if isinstance(filters['from_date'], str):
|
||||
final_filters['from_date'] = frappe.utils.getdate(filters['from_date'])
|
||||
else:
|
||||
final_filters['from_date'] = filters['from_date']
|
||||
frappe.log_error(f"ОТЧЕТ API - используем стандартное поле from_date: {final_filters['from_date']}", "Report API Debug")
|
||||
|
||||
if 'to_date' not in final_filters and 'to_date' in filters:
|
||||
if isinstance(filters['to_date'], str):
|
||||
final_filters['to_date'] = frappe.utils.getdate(filters['to_date'])
|
||||
else:
|
||||
final_filters['to_date'] = filters['to_date']
|
||||
frappe.log_error(f"ОТЧЕТ API - используем стандартное поле to_date: {final_filters['to_date']}", "Report API Debug")
|
||||
|
||||
# Если нет дат, устанавливаем значения по умолчанию
|
||||
if 'from_date' not in final_filters:
|
||||
final_filters['from_date'] = frappe.utils.get_first_day(frappe.utils.nowdate())
|
||||
frappe.log_error(f"ОТЧЕТ API - устанавливаем from_date по умолчанию: {final_filters['from_date']}", "Report API Debug")
|
||||
|
||||
if 'to_date' not in final_filters:
|
||||
final_filters['to_date'] = frappe.utils.nowdate()
|
||||
frappe.log_error(f"ОТЧЕТ API - устанавливаем to_date по умолчанию: {final_filters['to_date']}", "Report API Debug")
|
||||
|
||||
# Специальная обработка для отчетов в зависимости от их типа
|
||||
if report_name.lower() in ["general ledger", "general_ledger"]:
|
||||
# Для General Ledger нужны поля from и to
|
||||
final_filters['from'] = final_filters['from_date']
|
||||
final_filters['to'] = final_filters['to_date']
|
||||
elif report_name.lower() in ["cash flow", "cash_flow"]:
|
||||
# Для Cash Flow нужны period_start_date и period_end_date
|
||||
final_filters['period_start_date'] = final_filters['from_date']
|
||||
final_filters['period_end_date'] = final_filters['to_date']
|
||||
# Также добавляем filter_based_on со значением "Date Range"
|
||||
final_filters['filter_based_on'] = "Date Range"
|
||||
# и periodicity со значением "Monthly"
|
||||
final_filters['periodicity'] = "Monthly"
|
||||
|
||||
frappe.log_error(f"ОТЧЕТ API - финальные фильтры: {final_filters}", "Report API Debug")
|
||||
|
||||
# Для указанных отчетов просто используем подготовленные фильтры
|
||||
if report_name.lower() in ["general ledger", "general_ledger", "cash flow", "cash_flow"]:
|
||||
standard_filters = final_filters
|
||||
custom_filters = {}
|
||||
else:
|
||||
# Для остальных отчетов используем обычное разделение
|
||||
standard_filters, custom_filters = generate_dynamic_filters(report_name, final_filters)
|
||||
|
||||
frappe.log_error(f"ОТЧЕТ API - стандартные фильтры: {standard_filters}", "Report API Debug")
|
||||
frappe.log_error(f"ОТЧЕТ API - пользовательские фильтры: {custom_filters}", "Report API Debug")
|
||||
|
||||
# Выполняем отчет
|
||||
from frappe.desk.query_report import run
|
||||
frappe.log_error(f"ОТЧЕТ API - запуск отчета {report_name} с фильтрами {standard_filters}", "Report API Debug")
|
||||
report_result = run(report_name, filters=standard_filters)
|
||||
|
||||
# Проверяем на ошибки
|
||||
if isinstance(report_result, dict) and report_result.get('message'):
|
||||
frappe.log_error(f"ОТЧЕТ API - получена ошибка: {report_result.get('message')}", "Report API Debug")
|
||||
return {"error": report_result.get('message')}
|
||||
|
||||
# Получаем результаты
|
||||
columns = report_result.get('columns', [])
|
||||
data = report_result.get('result', [])
|
||||
|
||||
frappe.log_error(f"ОТЧЕТ API - получено строк данных: {len(data)}", "Report API Debug")
|
||||
|
||||
# Применяем пользовательские фильтры, если они есть
|
||||
if custom_filters:
|
||||
frappe.log_error(f"ОТЧЕТ API - применяем пользовательские фильтры", "Report API Debug")
|
||||
data = apply_custom_filters(data, custom_filters, columns)
|
||||
frappe.log_error(f"ОТЧЕТ API - после фильтрации строк: {len(data)}", "Report API Debug")
|
||||
|
||||
# Если указано конкретное поле, возвращаем только его значение
|
||||
if field_name:
|
||||
frappe.log_error(f"ОТЧЕТ API - извлекаем поле: {field_name}", "Report API Debug")
|
||||
result = extract_field_from_report(data, columns, field_name)
|
||||
frappe.log_error(f"ОТЧЕТ API - результат извлечения: {result}", "Report API Debug")
|
||||
return result
|
||||
|
||||
# Иначе возвращаем полный результат
|
||||
frappe.log_error("ОТЧЕТ API - возвращаем полный результат", "Report API Debug")
|
||||
return {
|
||||
"columns": columns,
|
||||
"data": data
|
||||
}
|
||||
except Exception as e:
|
||||
frappe.log_error(f"ОТЧЕТ API - ошибка: {str(e)}\n{frappe.get_traceback()}", "Report API Debug")
|
||||
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:
|
||||
frappe.log_error(f"Ошибка при получении JS-фильтров: {str(e)}", "Formula Editor")
|
||||
# В случае ошибки, считаем все фильтры стандартными
|
||||
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):
|
||||
"""Извлекает значение конкретного поля из результатов отчета"""
|
||||
|
||||
# Логгируем входящие параметры
|
||||
frappe.log_error(f"ОТЧЕТ - field_name: {field_name}", "Report Debug")
|
||||
frappe.log_error(f"ОТЧЕТ - columns: {columns[:10] if columns else 'None'}", "Report Debug")
|
||||
frappe.log_error(f"ОТЧЕТ - data count: {len(data) if data else 0}", "Report Debug")
|
||||
|
||||
if data and len(data) > 0:
|
||||
frappe.log_error(f"ОТЧЕТ - first row type: {type(data[0])}", "Report Debug")
|
||||
frappe.log_error(f"ОТЧЕТ - first row content: {data[0]}", "Report Debug")
|
||||
|
||||
# Проверяем, есть ли данные
|
||||
if not data:
|
||||
frappe.log_error("ОТЧЕТ - No data returned", "Report Debug")
|
||||
return 0
|
||||
|
||||
total = 0
|
||||
count = 0
|
||||
|
||||
# Определяем индекс колонки по имени поля
|
||||
column_idx = None
|
||||
for i, col in enumerate(columns):
|
||||
frappe.log_error(f"ОТЧЕТ - checking column {i}: {col}", "Report Debug")
|
||||
|
||||
if isinstance(col, dict):
|
||||
if col.get('fieldname') == field_name or col.get('label') == field_name:
|
||||
column_idx = i
|
||||
frappe.log_error(f"ОТЧЕТ - found column by name/label at index {i}", "Report Debug")
|
||||
break
|
||||
elif isinstance(col, str) and (col == field_name or field_name.lower() in col.lower()):
|
||||
column_idx = i
|
||||
frappe.log_error(f"ОТЧЕТ - found column by string at index {i}: {col}", "Report Debug")
|
||||
break
|
||||
|
||||
frappe.log_error(f"ОТЧЕТ - column_idx: {column_idx}", "Report Debug")
|
||||
|
||||
# Если колонка не найдена, ищем ее по любым подстрокам в имени
|
||||
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
|
||||
frappe.log_error(f"ОТЧЕТ - found column by partial match at index {i}: {col_name}", "Report Debug")
|
||||
break
|
||||
|
||||
# Если колонка найдена, суммируем значения
|
||||
if column_idx is not None:
|
||||
for idx, row in enumerate(data[:10]): # Логируем только первые 10 строк для производительности
|
||||
frappe.log_error(f"ОТЧЕТ - processing row {idx}: {row}", "Report Debug")
|
||||
|
||||
if isinstance(row, dict) and field_name in row:
|
||||
value = row[field_name]
|
||||
frappe.log_error(f"ОТЧЕТ - dict row value: {value}", "Report Debug")
|
||||
elif not isinstance(row, dict) and column_idx < len(row):
|
||||
value = row[column_idx]
|
||||
frappe.log_error(f"ОТЧЕТ - list row value at index {column_idx}: {value}", "Report Debug")
|
||||
else:
|
||||
# Дополнительная проверка для словарей - может, поле есть с другим регистром
|
||||
if isinstance(row, dict):
|
||||
# Проходим по всем ключам и ищем соответствие без учета регистра
|
||||
for key in row:
|
||||
if key.lower() == field_name.lower() or "balance" in key.lower():
|
||||
value = row[key]
|
||||
frappe.log_error(f"ОТЧЕТ - found value by case-insensitive key {key}: {value}", "Report Debug")
|
||||
break
|
||||
else:
|
||||
frappe.log_error(f"ОТЧЕТ - no value found in row, available keys: {list(row.keys()) if isinstance(row, dict) else 'N/A'}", "Report Debug")
|
||||
continue
|
||||
else:
|
||||
frappe.log_error(f"ОТЧЕТ - no value found in row", "Report Debug")
|
||||
continue
|
||||
|
||||
# Пытаемся преобразовать в число
|
||||
try:
|
||||
if value is not None:
|
||||
num_value = float(value)
|
||||
total += num_value
|
||||
count += 1
|
||||
frappe.log_error(f"ОТЧЕТ - added value: {num_value}, total now: {total}", "Report Debug")
|
||||
except (ValueError, TypeError):
|
||||
frappe.log_error(f"ОТЧЕТ - could not convert value to float: {value}", "Report Debug")
|
||||
pass
|
||||
else:
|
||||
# Если колонка не найдена, попробуем найти первое числовое поле как заглушку
|
||||
frappe.log_error(f"ОТЧЕТ - column not found, trying to find any numeric field", "Report Debug")
|
||||
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)
|
||||
frappe.log_error(f"ОТЧЕТ - found numeric value in key {key}: {num_value}", "Report Debug")
|
||||
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)
|
||||
frappe.log_error(f"ОТЧЕТ - found numeric value at index {i}: {num_value}", "Report Debug")
|
||||
total += num_value
|
||||
count += 1
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
frappe.log_error(f"ОТЧЕТ - final total: {total}, count: {count}", "Report Debug")
|
||||
# Возвращаем сумму или 0, если не удалось найти
|
||||
return total
|
||||
|
||||
@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'")
|
||||
|
||||
# Специальная обработка для отчета 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"}
|
||||
]
|
||||
|
||||
# Для остальных отчетов - стандартная обработка
|
||||
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("Отчет не вернул результатов")
|
||||
|
||||
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 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']):
|
||||
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"
|
||||
})
|
||||
|
||||
if not fields:
|
||||
frappe.throw(f"В отчете не найдено числовых полей. Пожалуйста, выберите другой отчет или свяжитесь с администратором.")
|
||||
|
||||
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:
|
||||
# Получаем документ отчета
|
||||
from frappe.desk.query_report import get_report_doc, get_script
|
||||
report_doc = get_report_doc(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:
|
||||
# Пытаемся запустить отчет с минимальными фильтрами
|
||||
# Базовые фильтры, которые часто требуются отчетам
|
||||
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:
|
||||
frappe.log_error(f"Ошибка при выполнении отчета с базовыми фильтрами: {str(e)}", "Formula Editor")
|
||||
|
||||
# Анализируем ошибку на предмет недостающих фильтров
|
||||
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)
|
||||
|
||||
# Получаем дефолтную компанию
|
||||
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 # Скрываем фильтр компании всегда
|
||||
else:
|
||||
# Добавляем скрытый фильтр компании, если его нет
|
||||
formatted_filters.append({
|
||||
'fieldname': 'company',
|
||||
'label': 'Company',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Company',
|
||||
'default': default_company,
|
||||
'reqd': 1,
|
||||
'hidden': 1 # Скрываем фильтр компании всегда
|
||||
})
|
||||
|
||||
return formatted_filters
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"Ошибка при получении фильтров отчета: {str(e)}", "Formula Editor")
|
||||
return []
|
||||
|
||||
|
||||
def extract_filter_names_from_error(error_msg):
|
||||
"""Извлечь имена отсутствующих фильтров из сообщения об ошибке"""
|
||||
filter_names = set()
|
||||
|
||||
# Типичные шаблоны ошибок в отчетах Frappe
|
||||
import re
|
||||
|
||||
# Например: "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)
|
||||
|
||||
# Например: "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)
|
||||
|
||||
# Например: "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)
|
||||
|
||||
return list(filter_names)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,6 +9,7 @@
|
|||
"formula_storage",
|
||||
"universal_fields_storage",
|
||||
"table_row_formulas_storage",
|
||||
"report_fields_storage",
|
||||
"formula_fields",
|
||||
"test"
|
||||
],
|
||||
|
|
@ -42,11 +43,16 @@
|
|||
"fieldname": "table_row_formulas_storage",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "table_row_formulas_storage"
|
||||
},
|
||||
{
|
||||
"fieldname": "report_fields_storage",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "report_fields_storage"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-03 16:25:00.731153",
|
||||
"modified": "2025-04-08 13:22:43.700556",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Formula Editor",
|
||||
"name": "Formula Editor",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
frappe.ui.form.on('Report Simulator', {
|
||||
refresh: function(frm) {
|
||||
// Добавляем кнопку для выполнения отчета
|
||||
frm.add_custom_button(__('Выполнить отчет'), function() {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'execute_report',
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frappe.msgprint({
|
||||
title: __('Результат отчета'),
|
||||
indicator: 'green',
|
||||
message: `<pre style="max-height: 500px; overflow-y: auto;">${r.message}</pre>`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Добавляем кнопку для открытия конструктора фильтров
|
||||
frm.add_custom_button(__('Настроить фильтры'), function() {
|
||||
show_filter_dialog(frm);
|
||||
});
|
||||
},
|
||||
|
||||
report: function(frm) {
|
||||
// При выборе отчета загружаем его фильтры
|
||||
if (frm.doc.report) {
|
||||
load_report_filters(frm);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Функция для загрузки фильтров отчета
|
||||
function load_report_filters(frm) {
|
||||
frappe.call({
|
||||
method: 'formula_editor.formula_editor.doctype.report_simulator.report_simulator.get_report_filters_dynamic',
|
||||
args: {
|
||||
report_name: frm.doc.report
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Загрузка фильтров отчета...'),
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frm.report_filters_meta = r.message;
|
||||
// Устанавливаем пустой фильтр по умолчанию, если его нет
|
||||
if (!frm.doc.filters_json) {
|
||||
frm.set_value('filters_json', '{}');
|
||||
}
|
||||
|
||||
// Показываем уведомление о количестве доступных фильтров
|
||||
frappe.show_alert({
|
||||
message: __(`Загружено ${r.message.length} фильтров для отчета`),
|
||||
indicator: 'green'
|
||||
}, 3);
|
||||
} else {
|
||||
frm.report_filters_meta = [];
|
||||
// Показываем уведомление, если фильтры не найдены
|
||||
frappe.show_alert({
|
||||
message: __('Для этого отчета не найдены фильтры. Используйте формат JSON для настройки фильтров.'),
|
||||
indicator: 'orange'
|
||||
}, 5);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для отображения диалога с динамическими фильтрами
|
||||
function show_filter_dialog(frm) {
|
||||
if (!frm.doc.report) {
|
||||
frappe.throw(__('Пожалуйста, сначала выберите отчет'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Если фильтры еще не загружены, загружаем их
|
||||
if (!frm.report_filters_meta) {
|
||||
load_report_filters(frm);
|
||||
frappe.throw(__('Пожалуйста, подождите загрузки фильтров и попробуйте снова'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Загружаем текущие значения фильтров
|
||||
let current_filters = {};
|
||||
try {
|
||||
current_filters = JSON.parse(frm.doc.filters_json || '{}');
|
||||
} catch (e) {
|
||||
current_filters = {};
|
||||
}
|
||||
|
||||
// Создаем поля для диалога на основе метаданных фильтров
|
||||
let fields = [];
|
||||
|
||||
// Если есть метаданные фильтров, создаем динамические поля
|
||||
if (frm.report_filters_meta && frm.report_filters_meta.length > 0) {
|
||||
// Добавляем разделитель для динамических фильтров
|
||||
fields.push({
|
||||
fieldtype: 'Section Break',
|
||||
label: __('Динамические фильтры')
|
||||
});
|
||||
|
||||
// Создаем поля для каждого фильтра
|
||||
frm.report_filters_meta.forEach(filter => {
|
||||
// Преобразуем метаданные фильтра в поле диалога
|
||||
let field = {
|
||||
fieldname: filter.fieldname,
|
||||
label: __(filter.label || filter.fieldname),
|
||||
fieldtype: filter.fieldtype || 'Data',
|
||||
reqd: filter.reqd || 0,
|
||||
default: current_filters[filter.fieldname] || filter.default,
|
||||
options: filter.options,
|
||||
description: filter.description
|
||||
};
|
||||
|
||||
// Для дат устанавливаем форматирование
|
||||
if (field.fieldtype === 'Date' && field.default) {
|
||||
// Преобразуем строку в объект Date, если это необходимо
|
||||
if (typeof field.default === 'string') {
|
||||
try {
|
||||
field.default = frappe.datetime.str_to_obj(field.default);
|
||||
} catch (e) {
|
||||
// Если не удалось преобразовать, оставляем как есть
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем поле в список
|
||||
fields.push(field);
|
||||
|
||||
// Добавляем разрыв колонки после каждого второго поля для компактности
|
||||
if (fields.length % 2 === 0) {
|
||||
fields.push({ fieldtype: 'Column Break' });
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем разделитель для ручного редактирования JSON
|
||||
fields.push({
|
||||
fieldtype: 'Section Break',
|
||||
label: __('Ручное редактирование')
|
||||
});
|
||||
}
|
||||
|
||||
// Всегда добавляем JSON-редактор для расширенного редактирования
|
||||
fields.push({
|
||||
fieldtype: 'Code',
|
||||
fieldname: 'filters_json',
|
||||
label: __('Фильтры (JSON)'),
|
||||
default: JSON.stringify(current_filters, null, 2),
|
||||
options: 'JSON'
|
||||
});
|
||||
|
||||
// Создаем диалог с созданными полями
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Фильтры отчета'),
|
||||
fields: fields,
|
||||
primary_action_label: __('Применить'),
|
||||
primary_action(values) {
|
||||
try {
|
||||
// Если есть динамические фильтры, собираем их значения
|
||||
let final_filters = {};
|
||||
|
||||
// Проверяем наличие метаданных фильтров
|
||||
if (frm.report_filters_meta && frm.report_filters_meta.length > 0) {
|
||||
// Собираем значения динамических фильтров
|
||||
frm.report_filters_meta.forEach(filter => {
|
||||
let value = values[filter.fieldname];
|
||||
|
||||
// Пропускаем пустые значения для необязательных полей
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
final_filters[filter.fieldname] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем, был ли изменен JSON вручную
|
||||
let manual_json = {};
|
||||
try {
|
||||
manual_json = JSON.parse(values.filters_json);
|
||||
|
||||
// Если JSON был изменен вручную, используем его вместо динамических фильтров
|
||||
if (JSON.stringify(manual_json) !== JSON.stringify(current_filters)) {
|
||||
final_filters = manual_json;
|
||||
}
|
||||
} catch (e) {
|
||||
frappe.throw(__('Некорректный формат JSON'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Устанавливаем итоговое значение фильтров
|
||||
frm.set_value('filters_json', JSON.stringify(final_filters, null, 2));
|
||||
d.hide();
|
||||
|
||||
// Показываем уведомление об успешном применении фильтров
|
||||
frappe.show_alert({
|
||||
message: __('Фильтры успешно применены'),
|
||||
indicator: 'green'
|
||||
}, 3);
|
||||
} catch (e) {
|
||||
frappe.throw(__(`Ошибка при применении фильтров: ${e.message}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем обработчик событий для обновления JSON при изменении динамических полей
|
||||
if (frm.report_filters_meta && frm.report_filters_meta.length > 0) {
|
||||
frm.report_filters_meta.forEach(filter => {
|
||||
d.fields_dict[filter.fieldname].df.change = function() {
|
||||
update_json_from_fields(d);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для обновления JSON на основе значений полей
|
||||
function update_json_from_fields(dialog) {
|
||||
let updated_filters = {};
|
||||
|
||||
// Собираем значения всех динамических полей
|
||||
frm.report_filters_meta.forEach(filter => {
|
||||
let value = dialog.fields_dict[filter.fieldname].get_value();
|
||||
|
||||
// Добавляем только непустые значения
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
updated_filters[filter.fieldname] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем JSON-поле
|
||||
dialog.set_value('filters_json', JSON.stringify(updated_filters, null, 2));
|
||||
}
|
||||
|
||||
d.show();
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-04-08 16:25:04.515744",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"report",
|
||||
"filters_json",
|
||||
"result",
|
||||
"is_script_report"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "report",
|
||||
"fieldtype": "Link",
|
||||
"label": "Report",
|
||||
"options": "Report"
|
||||
},
|
||||
{
|
||||
"fieldname": "filters_json",
|
||||
"fieldtype": "Code",
|
||||
"label": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "result",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "Result"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_script_report",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-08 17:10:17.242850",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Formula Editor",
|
||||
"name": "Report Simulator",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,124 @@
|
|||
frappe.listview_settings['Report Simulator'] = {
|
||||
onload: function(listview) {
|
||||
listview.page.add_inner_button(__('Быстрая симуляция'), function() {
|
||||
// Открываем диалог для быстрой симуляции отчета
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Быстрая симуляция отчета'),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'report',
|
||||
label: __('Отчет'),
|
||||
options: 'Report',
|
||||
reqd: 1,
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
report_type: 'Script Report'
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype: 'Code',
|
||||
fieldname: 'filters_json',
|
||||
label: __('Фильтры (JSON)'),
|
||||
default: '{}',
|
||||
options: 'JSON'
|
||||
}
|
||||
],
|
||||
primary_action_label: __('Запустить'),
|
||||
primary_action(values) {
|
||||
if (!values.report) {
|
||||
frappe.throw(__('Пожалуйста, выберите отчет'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем формат JSON
|
||||
try {
|
||||
JSON.parse(values.filters_json);
|
||||
} catch (e) {
|
||||
frappe.throw(__('Некорректный формат JSON'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Выполняем отчет
|
||||
frappe.call({
|
||||
method: 'formula_editor.formula_editor.doctype.report_simulator.report_simulator.run_report_simulation',
|
||||
args: {
|
||||
report_name: values.report,
|
||||
filters_json: values.filters_json
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Выполнение отчета...'),
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
// Форматируем результат для отображения
|
||||
let columns = r.message.columns;
|
||||
let data = r.message.data;
|
||||
|
||||
// Создаем HTML-таблицу для отображения результата
|
||||
let html = '<div style="max-height: 500px; overflow-y: auto;">';
|
||||
html += '<table class="table table-bordered">';
|
||||
|
||||
// Заголовок таблицы
|
||||
html += '<thead><tr>';
|
||||
columns.forEach(col => {
|
||||
html += `<th>${col.label || col.fieldname}</th>`;
|
||||
});
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Тело таблицы
|
||||
html += '<tbody>';
|
||||
data.forEach(row => {
|
||||
html += '<tr>';
|
||||
columns.forEach(col => {
|
||||
let value = row[col.fieldname];
|
||||
|
||||
// Форматируем значение, если это нужно
|
||||
if (value === null || value === undefined) {
|
||||
value = '';
|
||||
} else if (typeof value === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
|
||||
html += `<td>${value}</td>`;
|
||||
});
|
||||
html += '</tr>';
|
||||
});
|
||||
html += '</tbody>';
|
||||
html += '</table>';
|
||||
html += '</div>';
|
||||
|
||||
// Отображаем результат
|
||||
frappe.msgprint({
|
||||
title: __('Результат отчета'),
|
||||
indicator: 'green',
|
||||
message: html
|
||||
});
|
||||
|
||||
// Сохраняем симуляцию для истории
|
||||
frappe.db.insert({
|
||||
doctype: 'Report Simulator',
|
||||
report: values.report,
|
||||
filters_json: values.filters_json,
|
||||
result: JSON.stringify(r.message, null, 2)
|
||||
}).then(doc => {
|
||||
frappe.show_alert({
|
||||
message: __('Симуляция сохранена'),
|
||||
indicator: 'green'
|
||||
}, 3);
|
||||
listview.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
|
||||
d.show();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright (c) 2025, Jey ERP and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record depdendencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestReportSimulator(UnitTestCase):
|
||||
"""
|
||||
Unit tests for ReportSimulator.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestReportSimulator(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for ReportSimulator.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
Loading…
Reference in New Issue