576 lines
23 KiB
Python
576 lines
23 KiB
Python
import frappe
|
||
import json
|
||
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):
|
||
"""Получение числовых полей DocType для создания формул"""
|
||
meta = frappe.get_meta(doctype)
|
||
fields = []
|
||
|
||
# Получаем все числовые поля
|
||
for field in meta.fields:
|
||
if field.fieldtype in ["Int", "Float", "Currency"]:
|
||
fields.append({
|
||
"fieldname": field.fieldname,
|
||
"label": field.label,
|
||
"fieldtype": field.fieldtype
|
||
})
|
||
|
||
# Проверяем таблицы внутри DocType
|
||
for child in meta.fields:
|
||
if child.fieldtype == "Table":
|
||
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}",
|
||
"fieldtype": sub_field.fieldtype
|
||
})
|
||
|
||
return fields
|
||
|
||
@frappe.whitelist()
|
||
def get_all_doctype_fields(doctype):
|
||
"""Получение отфильтрованных полей DocType включая расширенные типы для фильтров"""
|
||
meta = frappe.get_meta(doctype)
|
||
fields = []
|
||
|
||
# Типы полей, которые нужно исключить
|
||
excluded_types = [
|
||
"Tab Break", "Column Break", "Button", "Section Break", "Column Break",
|
||
"HTML", "Table", "Attach", "Attach Image", "Code", "Text Editor", "Image",
|
||
"Heading", "Fold", "Table MultiSelect"
|
||
]
|
||
|
||
# Получаем все поля, которые не входят в исключенные типы
|
||
for field in meta.fields:
|
||
if field.fieldname and field.fieldtype not in excluded_types:
|
||
field_data = {
|
||
"fieldname": field.fieldname,
|
||
"label": field.label or field.fieldname,
|
||
"fieldtype": field.fieldtype
|
||
}
|
||
|
||
# Добавляем опции для полей типа Select и Link
|
||
if field.fieldtype in ["Select", "Link"] and field.options:
|
||
field_data["options"] = field.options
|
||
|
||
fields.append(field_data)
|
||
|
||
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 get_report_filters_dynamic(report_name):
|
||
"""Универсальное получение фильтров отчета"""
|
||
if not report_name:
|
||
return []
|
||
|
||
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")
|
||
|
||
try:
|
||
result = run(report_name, filters=filters)
|
||
except Exception as e:
|
||
# Если отчет не выполняется с фильтрами, пробуем с базовыми
|
||
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)
|
||
|
||
# Выполняем отчет
|
||
from frappe.desk.query_report import run
|
||
report_result = run(report_name, filters=filters)
|
||
|
||
# Проверяем результат на ошибки
|
||
if isinstance(report_result, dict) and report_result.get('message'):
|
||
return {"error": report_result.get('message')}
|
||
|
||
columns = report_result.get('columns', [])
|
||
data = report_result.get('result', [])
|
||
|
||
# Если указано конкретное поле, извлекаем его значение
|
||
if field_name:
|
||
result = extract_field_from_report(data, columns, field_name)
|
||
return result
|
||
|
||
return {
|
||
"columns": columns,
|
||
"data": data
|
||
}
|
||
|
||
except Exception as e:
|
||
frappe.log_error(f"Error in run_report_for_formula: {str(e)}")
|
||
return {"error": str(e)}
|
||
|
||
def extract_field_from_report(data, columns, field_name):
|
||
"""Универсальное извлечение значения поля из результатов отчета"""
|
||
if not data:
|
||
return 0
|
||
|
||
# Определяем индекс колонки по имени поля
|
||
column_idx = None
|
||
for i, col in enumerate(columns):
|
||
if isinstance(col, dict):
|
||
if col.get('fieldname') == field_name or col.get('label') == field_name:
|
||
column_idx = i
|
||
break
|
||
elif isinstance(col, str) and (col == field_name or field_name.lower() in col.lower()):
|
||
column_idx = i
|
||
break
|
||
|
||
# Если колонка не найдена, ищем по частичному совпадению
|
||
if column_idx is None:
|
||
for i, col in enumerate(columns):
|
||
col_name = ""
|
||
if isinstance(col, dict):
|
||
col_name = col.get('fieldname', '') or col.get('label', '')
|
||
elif isinstance(col, str):
|
||
col_name = col
|
||
|
||
if field_name.lower() in col_name.lower():
|
||
column_idx = i
|
||
break
|
||
|
||
if column_idx is not None:
|
||
# Ищем итоговые строки сначала
|
||
total_markers = ['total', 'итого', 'grand total', 'общий итог', 'итого по', 'total row']
|
||
|
||
for row in data:
|
||
is_total_row = False
|
||
|
||
if isinstance(row, dict):
|
||
# Проверяем специальные атрибуты итоговых строк
|
||
if (row.get('is_total_row') or
|
||
row.get('bold') or
|
||
row.get('total_row')):
|
||
is_total_row = True
|
||
|
||
# Проверяем ключи и значения на наличие маркеров итога
|
||
if not is_total_row:
|
||
for key, value in row.items():
|
||
if isinstance(value, str) and any(marker in value.lower() for marker in total_markers):
|
||
is_total_row = True
|
||
break
|
||
|
||
elif isinstance(row, list) and len(row) > 0:
|
||
# Для списков проверяем первый элемент
|
||
first_element = str(row[0]) if row[0] is not None else ""
|
||
if any(marker in first_element.lower() for marker in total_markers):
|
||
is_total_row = True
|
||
|
||
# Если нашли итоговую строку, извлекаем значение
|
||
if is_total_row:
|
||
if isinstance(row, dict):
|
||
if field_name in row:
|
||
value = row[field_name]
|
||
else:
|
||
# Поиск с игнорированием регистра
|
||
value = None
|
||
for key in row:
|
||
if key.lower() == field_name.lower():
|
||
value = row[key]
|
||
break
|
||
elif isinstance(row, list) and column_idx < len(row):
|
||
value = row[column_idx]
|
||
else:
|
||
value = None
|
||
|
||
# Преобразуем в число
|
||
try:
|
||
if value is not None:
|
||
return float(value)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# Если итоговую строку не нашли, суммируем все значения
|
||
total = 0
|
||
count = 0
|
||
|
||
for row in data:
|
||
value = None
|
||
if isinstance(row, dict) and field_name in row:
|
||
value = row[field_name]
|
||
elif isinstance(row, dict):
|
||
# Поиск поля с игнорированием регистра
|
||
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:
|
||
if value is not None:
|
||
num_value = float(value)
|
||
total += num_value
|
||
count += 1
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
return total
|
||
|
||
return 0
|
||
|
||
@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_%"]
|
||
},
|
||
fields=["name"]
|
||
)
|
||
|
||
# Удаляем найденные скрипты
|
||
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):
|
||
"""Генерирует JavaScript функцию для работы с полями отчетов"""
|
||
if not report_fields:
|
||
return ""
|
||
|
||
return f"""
|
||
async function getReportFieldValue(frm, reportFieldName) {{
|
||
try {{
|
||
// Получаем информацию о поле отчета
|
||
const reportFields = {json.dumps(report_fields)};
|
||
const fieldInfo = reportFields[reportFieldName];
|
||
|
||
if (!fieldInfo) {{
|
||
console.warn('Report field not found:', reportFieldName);
|
||
return 0;
|
||
}}
|
||
|
||
// Парсим фильтры
|
||
let filters = {{}};
|
||
try {{
|
||
filters = JSON.parse(fieldInfo.filters_json || '{{}}');
|
||
}} catch (e) {{
|
||
console.error('Error parsing filters JSON:', e);
|
||
return 0;
|
||
}}
|
||
|
||
// Обрабатываем фильтры с возможными ссылками на поля документа
|
||
const processedFilters = {{}};
|
||
|
||
for (const key in filters) {{
|
||
let value = filters[key];
|
||
|
||
// Проверяем, является ли значение ссылкой на поле документа
|
||
if (typeof value === 'string' && value.startsWith('doc.')) {{
|
||
const docField = value.substring(4);
|
||
if (frm.doc[docField] !== undefined) {{
|
||
processedFilters[key] = frm.doc[docField];
|
||
continue;
|
||
}}
|
||
}}
|
||
|
||
// Обычные значения
|
||
processedFilters[key] = value;
|
||
}}
|
||
|
||
// Проверяем кеш
|
||
if (!window.reportCache) window.reportCache = {{}};
|
||
|
||
const cacheKey = `${{fieldInfo.report}}|${{JSON.stringify(processedFilters)}}|${{fieldInfo.field_name}}`;
|
||
const now = new Date().getTime();
|
||
|
||
if (window.reportCache[cacheKey] &&
|
||
window.reportCache[cacheKey].timestamp > now - 5 * 60 * 1000) {{
|
||
return window.reportCache[cacheKey].value;
|
||
}}
|
||
|
||
// Выполняем запрос к серверу
|
||
try {{
|
||
const result = await frappe.call({{
|
||
method: "formula_editor.api.run_report_for_formula",
|
||
args: {{
|
||
report_name: fieldInfo.report,
|
||
filters: processedFilters,
|
||
field_name: fieldInfo.field_name
|
||
}},
|
||
freeze: false
|
||
}});
|
||
|
||
if (result && result.message !== undefined) {{
|
||
if (result.message.error) {{
|
||
console.error('Report error:', result.message.error);
|
||
return 0;
|
||
}}
|
||
|
||
let value = parseFloat(result.message);
|
||
if (isNaN(value)) value = 0;
|
||
|
||
// Сохраняем в кеш
|
||
window.reportCache[cacheKey] = {{
|
||
value: value,
|
||
timestamp: now
|
||
}};
|
||
|
||
return value;
|
||
}}
|
||
}} catch (err) {{
|
||
console.error('Report field error:', err);
|
||
}}
|
||
|
||
return 0;
|
||
}} catch (error) {{
|
||
console.error('getReportFieldValue error:', error);
|
||
return 0;
|
||
}}
|
||
}}
|
||
""" |