Added report support, but have a lot of bugs

This commit is contained in:
Ali 2025-04-14 22:51:18 +04:00
parent 1680da6f31
commit 30f878fc82
9 changed files with 4330 additions and 282 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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