From e07740ce6da875e62079f0dab97e369cb3d24fe2 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Tue, 22 Apr 2025 11:36:20 +0400 Subject: [PATCH] Fully added report system --- formula_editor/api.py | 107 +- .../doctype/formula_editor/formula_editor.js | 1022 ++++++++++------- .../report_simulator/report_simulator.json | 2 +- 3 files changed, 681 insertions(+), 450 deletions(-) diff --git a/formula_editor/api.py b/formula_editor/api.py index 3b8f1c8..22c9698 100644 --- a/formula_editor/api.py +++ b/formula_editor/api.py @@ -91,31 +91,26 @@ 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") - - # Преобразуем фильтры, если они пришли в виде строки + frappe.log_error(f"ОТЧЕТ API - сырые фильтры (тип: {type(filters)}): {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 @@ -123,101 +118,87 @@ def run_report_for_formula(report_name, filters, field_name=None): 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'] + final_filters['from_date'] = frappe.utils.getdate(filters['from_date']) if isinstance(filters['from_date'], str) else 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'] + final_filters['to_date'] = frappe.utils.getdate(filters['to_date']) if isinstance(filters['to_date'], str) else 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" - + + # === Универсальная логика для подстановки доп. полей на основе даты === + has_date_filters = 'from_date' in final_filters or 'to_date' in final_filters + report_name_lc = report_name.lower() + + if has_date_filters: + # Применяем поля from и to для всех отчетов, кроме cash flow + if report_name_lc not in ["cash flow", "cash_flow"]: + final_filters['from'] = final_filters.get('from_date') + final_filters['to'] = final_filters.get('to_date') + # Специальная обработка только для cash flow + elif report_name_lc in ["cash flow", "cash_flow"]: + final_filters['period_start_date'] = final_filters.get('from_date') + final_filters['period_end_date'] = final_filters.get('to_date') + final_filters['filter_based_on'] = "Date Range" + 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"]: + + # === Универсальное поведение: если есть дата-фильтры — используем их напрямую === + if has_date_filters: 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, @@ -226,7 +207,7 @@ def run_report_for_formula(report_name, filters, field_name=None): 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): """Обрабатывает фильтры для отчета, разделяя их на стандартные и пользовательские""" diff --git a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js index f405114..b612467 100644 --- a/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js +++ b/formula_editor/formula_editor/doctype/formula_editor/formula_editor.js @@ -267,7 +267,6 @@ function generate_client_script(frm) { let result = eval(transformResult.transformed); return isNaN(result) ? 0 : result; } catch (e) { - console.error("Error processing formula:", formula, e); return 0; } }`); @@ -408,7 +407,6 @@ function generate_client_script(frm) { const result = eval(finalExpression); frm.set_value("${fieldname}", isNaN(result) ? 0 : result); } catch (e) { - console.error("Error calculating ${fieldname}:", e); } }`); @@ -479,23 +477,35 @@ function generate_client_script(frm) { // Если есть формулы для строк табличных частей, инициализируем их if (Object.keys(table_row_formulas).length > 0) { refreshHandlerBody += ` initialize_table_row_formulas(frm);\n`; - - // Добавляем явный вызов для каждой формулы строки с триггером "При открытии документа" - Object.keys(table_row_formulas).forEach(id => { - const formula = table_row_formulas[id]; - if (formula.triggers && formula.triggers.split(',').map(t => t.trim()).some(t => isPredefinedEvent(t))) { - const usesUniversal = formula.formula.includes("univ("); - const usesReports = formula.formula.includes("report("); - - if (usesUniversal || usesReports) { - refreshHandlerBody += ` await calculate_table_row_field(frm, "${formula.table_name}", ${parseInt(formula.row_index) - 1}, "${formula.field_name}", "${formula.formula}", "refresh");\n`; - } else { - refreshHandlerBody += ` calculate_table_row_field(frm, "${formula.table_name}", ${parseInt(formula.row_index) - 1}, "${formula.field_name}", "${formula.formula}", "refresh");\n`; - } - } - }); } - + + // Добавляем вызов расчета универсальных полей, если они используются + if (refreshHandlers.some(h => h.uses_universal)) { + refreshHandlerBody += ` await calculate_universal_fields(frm);\n`; + } + + // Добавляем вызовы для каждого поля с триггером "При открытии документа" + refreshHandlers.forEach(handler => { + if (!handler.type || handler.type !== 'table_row') { + refreshHandlerBody += ` ${needsAsync && handler.uses_universal ? 'await ' : ''}calculate_${handler.field}(frm);\n`; + } + }); + + // Добавляем явный вызов для каждой формулы строки с триггером "При открытии документа" + Object.keys(table_row_formulas).forEach(id => { + const formula = table_row_formulas[id]; + if (formula.triggers && formula.triggers.split(',').map(t => t.trim()).some(t => isPredefinedEvent(t))) { + const usesUniversal = formula.formula.includes("univ("); + const usesReports = formula.formula.includes("report("); + + if (usesUniversal || usesReports) { + refreshHandlerBody += ` await calculate_table_row_field(frm, "${formula.table_name}", ${parseInt(formula.row_index) - 1}, "${formula.field_name}", "${formula.formula}", "refresh");\n`; + } else { + refreshHandlerBody += ` calculate_table_row_field(frm, "${formula.table_name}", ${parseInt(formula.row_index) - 1}, "${formula.field_name}", "${formula.formula}", "refresh");\n`; + } + } + }); + refreshHandlerBody += ' },\n'; // Добавляем обработчик refresh в начало скрипта @@ -538,38 +548,120 @@ function generate_client_script(frm) { } function generateReportFieldsFunction(report_fields) { - return ` - // Функция для получения значения поля отчета - async function getReportFieldValue(frm, reportName, filters, fieldName, aggregation = 'sum') { - try { + - // Проверка: если используется $AyIlSpecial, но ay или il не заполнены — возвращаем 0 - if (Object.values(filters).some(v => v === "$AyIlSpecial")) { - if (!frm.doc["ay"] || !frm.doc["il"]) { + return ` + window.notifiedAboutAyIl = false; + window.notifiedAboutRubIl = false; +async function getReportFieldValue(frm, reportName, filters, fieldName, aggregation = 'sum') { + try { + // Отладочное сообщение о начальных фильтрах + console.log(\`Запуск отчета \${reportName} с начальными фильтрами:\`, JSON.stringify(filters)); + + // Проверка необходимости полей сразу для всех фильтров + if (Object.values(filters).some(v => v === "$AyIlSpecial")) { + if (!frm.doc["ay"] || !frm.doc["il"]) { + if (!notifiedAboutAyIl) { frappe.msgprint({ title: "Not enough data", message: "Please fill in the ay and il fields", indicator: "orange" }); - return 0; + notifiedAboutAyIl = true; + } + return 0; + } + } + + // Проверка: если используется $RubIlSpecial, но rüb или il не заполнены — возвращаем 0 + if (Object.values(filters).some(v => v === "$RubIlSpecial")) { + if (!frm.doc["rüb"] || !frm.doc["il"]) { + if (!notifiedAboutRubIl) { + frappe.msgprint({ + title: "Not enough data", + message: "Please fill in the rüb and il fields", + indicator: "orange" + }); + notifiedAboutRubIl = true; + } + return 0; + } + } + + // Обрабатываем фильтры с возможными ссылками на поля документа + const processedFilters = {}; + + // Используем for...in loop для перебора свойств объекта filters + for (const key in filters) { + let value = filters[key]; + + // Специальная обработка для $DefaultCompany + if (value === "$DefaultCompany") { + processedFilters[key] = frm.doc.company || frappe.defaults.get_user_default("Company") || ""; + continue; + } + + // Специальная обработка для $AyIlSpecial + if (value === "$AyIlSpecial") { + // Проверяем, есть ли у документа поля ay и il + if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { + // Обрабатываем специальные поля ay и il для получения диапазона дат + const dateRange = processAzerbayjanDateField({ + ay: frm.doc["ay"], + il: frm.doc["il"] + }); + + // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия + if (dateRange) { + if (key.includes('from_date')) { + // Для начальной даты используем только >= + processedFilters[key] = dateRange.start; + } else if (key.includes('to_date')) { + // Для конечной даты используем только <= + processedFilters[key] = dateRange.end; + } else { + // Для других полей даты используем начало периода + processedFilters[key] = dateRange.start; + } + continue; + } } } - - // Обрабатываем фильтры с возможными ссылками на поля документа - const processedFilters = {}; - // Используем for...in loop для перебора свойств объекта filters - for (const key in filters) { - let value = filters[key]; - - // Специальная обработка для $DefaultCompany - if (value === "$DefaultCompany") { - value = frm.doc.company || frappe.defaults.get_user_default("Company") || ""; + // Специальная обработка для $RubIlSpecial + if (value === "$RubIlSpecial") { + // Проверяем, есть ли у документа поля rüb и il + if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { + // Обрабатываем специальные поля rüb и il для получения диапазона дат + const dateRange = processQuarterDateField({ + rüb: frm.doc["rüb"], + il: frm.doc["il"] + }); + + // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия + if (dateRange) { + if (key.includes('from_date')) { + // Для начальной даты используем только >= + processedFilters[key] = dateRange.start; + } else if (key.includes('to_date')) { + // Для конечной даты используем только <= + processedFilters[key] = dateRange.end; + } else { + // Для других полей даты используем начало периода + processedFilters[key] = dateRange.start; + } + continue; + } } + } + + // Проверяем, является ли значение ссылкой на поле документа + if (typeof value === 'string' && value.startsWith('doc.')) { + const docField = value.substring(4); // Убираем 'doc.' - // Специальная обработка для $AyIlSpecial - if (value === "$AyIlSpecial") { - // Проверяем, есть ли у документа поля ay и il + // Проверяем, является ли это специальным полем ay или il + if (docField === 'ay' || docField === 'il') { + // Получаем значения ay и il из документа if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { // Обрабатываем специальные поля ay и il для получения диапазона дат const dateRange = processAzerbayjanDateField({ @@ -577,139 +669,121 @@ function generateReportFieldsFunction(report_fields) { il: frm.doc["il"] }); - // Если удалось получить диапазон дат, заменяем фильтр только на нужные условия + // Если удалось получить диапазон дат, создаем правильные фильтры if (dateRange) { - // Удаляем исходный фильтр - delete processedFilters[key]; - - // Проверяем, относится ли фильтр к начальной или конечной дате if (key.includes('from_date')) { // Для начальной даты используем только >= - processedFilters[\`\${key}>\`] = dateRange.start; + processedFilters[key] = dateRange.start; } else if (key.includes('to_date')) { // Для конечной даты используем только <= - processedFilters[\`\${key}<\`] = dateRange.end; + processedFilters[key] = dateRange.end; } else { - // Для других полей используем оба условия как раньше - processedFilters[\`\${key}>\`] = dateRange.start; - processedFilters[\`\${key}<\`] = dateRange.end; + // Для других полей даты используем начало периода + processedFilters[key] = dateRange.start; } - - // Переходим к следующему фильтру - continue; + continue; } } - } - - // Проверяем, является ли значение ссылкой на поле документа - if (typeof value === 'string' && value.startsWith('doc.')) { - const docField = value.substring(4); // Убираем 'doc.' - - // Проверяем, является ли это специальным полем ay или il - if (docField === 'ay' || docField === 'il') { - // Получаем значения ay и il из документа - if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) { - // Обрабатываем специальные поля ay и il для получения диапазона дат - const dateRange = processAzerbayjanDateField({ - ay: frm.doc["ay"], - il: frm.doc["il"] - }); - - // Если удалось получить диапазон дат, создаем правильные фильтры - if (dateRange) { - // Удаляем исходный фильтр, если он есть - if (processedFilters.hasOwnProperty(key)) { - delete processedFilters[key]; - } - - // Проверяем, относится ли фильтр к начальной или конечной дате - if (key.includes('from_date')) { - // Для начальной даты используем только >= - processedFilters[\`\${key}>=\`] = dateRange.start; - } else if (key.includes('to_date')) { - // Для конечной даты используем только <= - processedFilters[\`\${key}<=\`] = dateRange.end; - } else { - // Для других полей используем оба условия - processedFilters[\`\${key}>=\`] = dateRange.start; - processedFilters[\`\${key}<=\`] = dateRange.end; - } - - // Переходим к следующему фильтру - continue; + } + // Проверяем, является ли это специальным полем rüb + else if (docField === 'rüb') { + // Получаем значения rüb и il из документа + if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { + // Обрабатываем специальные поля rüb и il для получения диапазона дат + const dateRange = processQuarterDateField({ + rüb: frm.doc["rüb"], + il: frm.doc["il"] + }); + + // Если удалось получить диапазон дат, создаем правильные фильтры + if (dateRange) { + if (key.includes('from_date')) { + // Для начальной даты используем только >= + processedFilters[key] = dateRange.start; + } else if (key.includes('to_date')) { + // Для конечной даты используем только <= + processedFilters[key] = dateRange.end; + } else { + // Для других полей даты используем начало периода + processedFilters[key] = dateRange.start; } + continue; } - } else if (frm.doc[docField] !== undefined) { - // Берем значение из документа - value = frm.doc[docField]; - } else { - // Если поле не найдено, пропускаем фильтр - continue; } + } + // Для других полей документа + else if (frm.doc[docField] !== undefined) { + // Берем значение из документа + processedFilters[key] = frm.doc[docField]; + continue; + } else { + // Если поле не найдено, пропускаем фильтр + continue; } - - // Добавляем обработанное значение в фильтры - processedFilters[key] = value; } - // Проверяем кеш для экономии запросов - if (!window.reportCache) window.reportCache = {}; - - const cacheKey = \`\${reportName}|\${JSON.stringify(processedFilters)}|\${fieldName}|\${aggregation}\`; - - // Если есть в кеше и кеш не устарел (5 минут) - const now = new Date().getTime(); - if (window.reportCache[cacheKey] && - window.reportCache[cacheKey].timestamp > now - 5 * 60 * 1000) { - return window.reportCache[cacheKey].value; - } - - console.log("DEBUG: Вызов getReportFieldValue для отчета:", reportName, "Фильтры:", processedFilters, "Поле:", fieldName); - - // Выполняем запрос к серверу с обработанными фильтрами - try { - const result = await frappe.call({ - method: "formula_editor.api.run_report_for_formula", - args: { - report_name: reportName, - filters: processedFilters, - field_name: fieldName - }, - freeze: false // Не блокируем интерфейс - }); - - // Проверяем результат - if (result && result.message) { - if (result.message.error) { - console.error(\`Ошибка при выполнении отчета \${reportName}:\`, 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(\`Ошибка вызова API для отчета \${reportName}:\`, err); - } - - return 0; - } catch (error) { - console.error(\`Ошибка при получении значения поля отчета:\`, error); - return 0; + // Добавляем обработанное значение в фильтры + processedFilters[key] = value; } - }`; + + // Отладочное сообщение об обработанных фильтрах + console.log(\`ОТЧЕТ API - запуск отчета \${reportName} с фильтрами\`, JSON.stringify(processedFilters)); + + // Проверяем кеш для экономии запросов + if (!window.reportCache) window.reportCache = {}; + + const cacheKey = \`\${reportName}|\${JSON.stringify(processedFilters)}|\${fieldName}|\${aggregation}\`; + + // Если есть в кеше и кеш не устарел (5 минут) + 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: reportName, + filters: processedFilters, + field_name: fieldName + }, + freeze: false // Не блокируем интерфейс + }); + + // Проверяем результат + if (result && result.message) { + if (result.message.error) { + console.log(\`Ошибка при получении данных отчета: \${result.message.error}\`); + return 0; + } + + // Получаем числовое значение + let value = parseFloat(result.message); + if (isNaN(value)) value = 0; + + console.log(\`Результат отчета \${reportName}: \${value}\`); + + // Сохраняем в кеш + window.reportCache[cacheKey] = { + value: value, + timestamp: now + }; + + return value; + } + } catch (err) { + console.error(\`Ошибка при вызове отчета \${reportName}:\`, err); + } + + return 0; + } catch (error) { + console.error(\`Общая ошибка в getReportFieldValue:\`, error); + return 0; + } +}`; } // Новая функция для генерации скриптов дочерних таблиц с расширенным логированием @@ -756,14 +830,12 @@ function generate_child_table_script(table_child_scripts, table_doctypes, table_ try { result = eval(finalExpression); } catch (e) { - console.error("Ошибка при вычислении формулы строки таблицы:", e, finalExpression); } // Обновляем значение row["${calc.field}"] = isNaN(result) ? 0 : result; frm.refresh_field("${tableName}"); } catch (error) { - console.error("Ошибка в calculate_${tableName}_row:", error); } }`; @@ -774,7 +846,6 @@ function generate_child_table_script(table_child_scripts, table_doctypes, table_ const transformed = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}").transformed; row["${calc.field}"] = eval(transformed); } catch (e) { - console.error("Ошибка при расчете поля ${calc.field} в строке таблицы ${tableName}:", e); row["${calc.field}"] = 0; } `; @@ -783,7 +854,6 @@ function generate_child_table_script(table_child_scripts, table_doctypes, table_ calcFunction += ` frm.refresh_field("${tableName}"); } catch (e) { - console.error("Ошибка при расчете строки таблицы ${tableName}:", e); } }`; @@ -885,8 +955,9 @@ function generateUniversalFieldsFunction(universal_fields) { newFilter.valueType = 'field'; newFilter.docField = docField; - // Проверяем, является ли это специальным полем ay или il + // Проверяем, является ли это специальным полем ay, il или rüb newFilter.__is_special_ay_field = (docField === 'ay' || docField === 'il'); + newFilter.__is_special_rub_field = (docField === 'rüb' || docField === 'il'); } else { newFilter.valueType = 'static'; } @@ -930,6 +1001,44 @@ function generateUniversalFieldsFunction(universal_fields) { // Обрабатываем специальные поля ay и il для получения диапазона дат const dateRange = processAzerbayjanDateField(docValues); + if (dateRange) { + // Добавляем два фильтра для диапазона дат + if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === ">=")) + filters_${fieldId}.push(["${filter.field}", ">=", dateRange.start]); + if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === "<")) + filters_${fieldId}.push(["${filter.field}", "<=", dateRange.end]); + } else { + skipCalculation_${fieldId} = true; + } + } else { + skipCalculation_${fieldId} = true; + } + `; + } else if (filter.__is_special_rub_field) { + return ` + // Фильтр со ссылкой на специальное поле квартала (rüb) или года (il) + if (frm.doc["rüb"] !== undefined && frm.doc["il"] !== undefined) { + // Получаем значения квартала и года + const docValues = { + rüb: frm.doc["rüb"], + il: frm.doc["il"] + }; + + // Проверяем, что поле rüb имеет допустимое значение + if (!docValues.rüb || typeof docValues.rüb !== 'string') { + skipCalculation_${fieldId} = true; + return; + } + + // Проверяем, что поле il имеет допустимое значение + if (!docValues.il || isNaN(parseInt(docValues.il))) { + skipCalculation_${fieldId} = true; + return; + } + + // Обрабатываем специальные поля rüb и il для получения диапазона дат + const dateRange = processQuarterDateField(docValues); + if (dateRange) { // Добавляем два фильтра для диапазона дат if (!filters_${fieldId}.some(f => f[0] === "${filter.field}" && f[1] === ">=")) @@ -992,7 +1101,72 @@ function generateUniversalFieldsFunction(universal_fields) { } `; - // Добавляем функцию для обработки специальных полей месяца + // Добавляем функцию для обработки специальных полей с кварталом + universalFieldsCode += ` + // Функция для обработки квартала (rüb) и года (il) + function processQuarterDateField(docValues) { + // Проверяем, есть ли у нас значения rüb и il + if (docValues && docValues.rüb && docValues.il) { + // Получаем номер квартала (преобразуем из строки вида "1 Rüb" в число 1) + const quarterStr = docValues.rüb; + const quarterMatch = quarterStr.match(/(\\d+)/); + + if (!quarterMatch) { + return null; + } + + const quarterNum = parseInt(quarterMatch[1]); + + // Получаем год из поля il или используем текущий год + let year = docValues.il || new Date().getFullYear(); + + // Определяем начальный и конечный месяц квартала + let startMonth, endMonth; + + switch(quarterNum) { + case 1: + startMonth = 1; // Январь + endMonth = 3; // Март + break; + case 2: + startMonth = 4; // Апрель + endMonth = 6; // Июнь + break; + case 3: + startMonth = 7; // Июль + endMonth = 9; // Сентябрь + break; + case 4: + startMonth = 10; // Октябрь + endMonth = 12; // Декабрь + break; + default: + return null; + } + + // Определяем первый день квартала (1-е число начального месяца) + const firstDay = new Date(year, startMonth - 1, 1); + + // Последний день квартала (последнее число конечного месяца) + const lastDay = new Date(year, endMonth, 0); + + // Форматируем даты в формате YYYY-MM-DD + const firstDayStr = \`\${year}-\${String(startMonth).padStart(2, '0')}-01\`; + const lastDayStr = \`\${year}-\${String(endMonth).padStart(2, '0')}-\${String(lastDay.getDate()).padStart(2, '0')}\`; + + // Возвращаем объект с диапазоном дат + return { + start: firstDayStr, + end: lastDayStr + }; + } + + // Если это не специальное поле или значение не распознано, возвращаем null + return null; + } + `; + + // Добавляем функцию для обработки специальных полей с азербайджанскими месяцами universalFieldsCode += ` // Функция для обработки специальных полей с азербайджанскими месяцами function processAzerbayjanDateField(docValues) { @@ -1012,6 +1186,12 @@ function generateUniversalFieldsFunction(universal_fields) { "Dekabr": 12 }; + // Проверяем наличие квартала (rüb) и года (il) + if (docValues && docValues.rüb && docValues.il) { + // Вызываем специальную функцию для обработки квартала + return processQuarterDateField(docValues); + } + // Проверяем, есть ли у нас значения ay и il if (docValues && docValues.ay && monthNames[docValues.ay]) { // Получаем номер месяца @@ -1026,7 +1206,7 @@ function generateUniversalFieldsFunction(universal_fields) { // Последний день месяца (получаем последнее число месяца) const lastDay = new Date(year, monthNum, 0); - // Форматируем даты в формате YYYY.MM.DD + // Форматируем даты в формате YYYY-MM-DD const firstDayStr = \`\${year}-\${String(monthNum).padStart(2, '0')}-01\`; const lastDayStr = \`\${year}-\${String(monthNum).padStart(2, '0')}-\${String(lastDay.getDate()).padStart(2, '0')}\`; @@ -1102,22 +1282,36 @@ function generateTransformExpressionFunction(universal_fields, report_fields) { // Создаем безопасную строку для JSON-фильтров let filtersJson = "{}"; try { + // Парсим JSON фильтров из поля отчета const filtersObj = JSON.parse(field.filters_json || "{}"); - // Заменяем значения компании и дат на маркеры - if (filtersObj.company && typeof filtersObj.company === 'string' && !filtersObj.company.startsWith('$')) { + // Заменяем значения компании на специальный маркер, если это не маркер + if (filtersObj.company && typeof filtersObj.company === 'string' && + !filtersObj.company.startsWith('$')) { filtersObj.company = "$DefaultCompany"; } - if (filtersObj.from_date && typeof filtersObj.from_date === 'string') { - filtersObj.from_date = "$AyIlSpecial"; - } - if (filtersObj.to_date && typeof filtersObj.to_date === 'string') { - filtersObj.to_date = "$AyIlSpecial"; - } - if (filtersObj.date && typeof filtersObj.date === 'string') { - filtersObj.date = "$AyIlSpecial"; - } + // Сохраняем оригинальные маркеры дат, если они уже есть + // Иначе устанавливаем маркер $AyIlSpecial как значение по умолчанию + Object.keys(filtersObj).forEach(key => { + if (key.toLowerCase().includes('date') || + key.toLowerCase().includes('from') || + key.toLowerCase().includes('to')) { + + const value = filtersObj[key]; + + // Если значение уже является маркером, оставляем его + if (typeof value === 'string' && + (value === "$AyIlSpecial" || value === "$RubIlSpecial" || + value.startsWith("doc."))) { + // Сохраняем оригинальное значение + } + // Если это обычная строка, заменяем на маркер $AyIlSpecial + else if (typeof value === 'string') { + filtersObj[key] = "$AyIlSpecial"; + } + } + }); // Конвертируем объект в строку JSON и экранируем спецсимволы filtersJson = JSON.stringify(filtersObj); @@ -1196,7 +1390,6 @@ function generateTransformExpressionFunction(universal_fields, report_fields) { }`; } - function processFunctionField(field_info, fieldname, functions, table_child_scripts, table_doctypes, main_script, target_doctype) { @@ -1300,14 +1493,12 @@ function generateTableScripts(table_child_scripts, functions) { try { result = eval(finalExpression); } catch (e) { - console.error("Ошибка при вычислении формулы строки таблицы:", e, finalExpression); } // Обновляем значение row["${calc.field}"] = isNaN(result) ? 0 : result; frm.refresh_field("${tableName}"); } catch (error) { - console.error("Ошибка в calculate_${tableName}_row:", error); } }`; @@ -1318,7 +1509,6 @@ function generateTableScripts(table_child_scripts, functions) { const transformed = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}").transformed; row["${calc.field}"] = eval(transformed); } catch (e) { - console.error("Ошибка при расчете поля ${calc.field} в строке таблицы ${tableName}:", e); row["${calc.field}"] = 0; } `; @@ -1327,7 +1517,6 @@ function generateTableScripts(table_child_scripts, functions) { calcFunction += ` frm.refresh_field("${tableName}"); } catch (e) { - console.error("Ошибка при расчете строки таблицы ${tableName}:", e); } }`; @@ -2668,7 +2857,6 @@ function handleDrop(e, $container, isTableField, frm) { // Проверяем, что данные не пустые if (!dataTransfer) { - console.error("Ошибка при перетаскивании: пустые данные"); return; } @@ -2720,7 +2908,6 @@ function handleDrop(e, $container, isTableField, frm) { initializeTokenEvents($newElement); } } catch (error) { - console.error("Ошибка при обработке перетаскивания:", error); } } @@ -4239,7 +4426,6 @@ function openUniversalFieldModal(frm, fieldId = null) { }); } } catch (error) { - console.error("Ошибка при инициализации обработчиков фильтров:", error); } // Если редактируем существующее поле, вызываем событие изменения doctype для загрузки полей @@ -4306,12 +4492,14 @@ function handleFilterFieldChange(dialog, grid_row, fieldInfo) { const targetDateFields = targetMeta.fields.filter(field => field.fieldtype === 'Date' || field.fieldname === 'ay' || - field.fieldname === 'il' + field.fieldname === 'il' || + field.fieldname === 'rüb' ); // Проверяем, есть ли специальные поля ay и il одновременно const hasAyField = targetDateFields.some(field => field.fieldname === 'ay'); const hasIlField = targetDateFields.some(field => field.fieldname === 'il'); + const hasRubField = targetDateFields.some(field => field.fieldname === 'rüb'); if (targetDateFields.length > 0) { // Создаем опции для полей с датой @@ -4328,6 +4516,13 @@ function handleFilterFieldChange(dialog, grid_row, fieldInfo) { }); } + if (hasRubField && hasIlField) { + options.push({ + label: 'Квартал (Rüb)', + value: 'doc.rüb' + }); + } + // Добавляем опцию для пользовательского ввода options.push({ label: 'Указать дату...', @@ -4419,6 +4614,12 @@ function handleFilterFieldChange(dialog, grid_row, fieldInfo) { grid_row.doc.__is_special_ay_field = true; } } + else if (value === 'doc.rüb') { + // Помечаем в data-атрибуте, что это специальное поле rüb + if (grid_row && grid_row.doc) { + grid_row.doc.__is_special_rub_field = true; + } + } } catch (error) { } }; @@ -5077,147 +5278,6 @@ function editUniversalField(frm, fieldId) { openUniversalFieldModal(frm, fieldId); } -// Функция для преобразования выражений формул -function transform_expression(expression, for_child_table = false, table_name = null) { - let transformed = expression; - let needsAsync = false; - let reportCalls = []; - - // === Обработка report(...) === - const reportRegex = /report\(([^)]+)\)/g; - let match; - - // Находим все вызовы report() в выражении - while ((match = reportRegex.exec(expression)) !== null) { - const reportName = match[1]; // Имя поля отчета - const fullMatch = match[0]; // Полный текст match - "report(xyz)" - needsAsync = true; - - // Создаем уникальный плейсхолдер - const placeholder = `__REPORT_${reportCalls.length}__`; - - // Ищем соответствующее поле отчета по имени - const reportFields = JSON.parse(cur_frm.doc.report_fields_storage || "{}"); - const reportField = Object.values(reportFields).find(f => f.name === reportName); - - if (reportField) { - // Безопасно обрабатываем JSON фильтров - let filtersJson = "{}"; - try { - const filtersObj = JSON.parse(reportField.filters_json || "{}"); - - // Заменяем значения компании и дат на маркеры - if (filtersObj.company && typeof filtersObj.company === 'string' && !filtersObj.company.startsWith('$')) { - filtersObj.company = "$DefaultCompany"; - } - - if (filtersObj.from_date && typeof filtersObj.from_date === 'string') { - filtersObj.from_date = "$AyIlSpecial"; - } - if (filtersObj.to_date && typeof filtersObj.to_date === 'string') { - filtersObj.to_date = "$AyIlSpecial"; - } - if (filtersObj.date && typeof filtersObj.date === 'string') { - filtersObj.date = "$AyIlSpecial"; - } - - // Конвертируем объект в строку JSON и экранируем спецсимволы - filtersJson = JSON.stringify(filtersObj); - } catch (e) { - filtersJson = "{}"; - } - - reportCalls.push({ - placeholder: placeholder, - reportName: reportField.report, - filters: JSON.parse(filtersJson), - fieldName: reportField.field_name, - aggregation: reportField.aggregation || 'sum' - }); - - // Заменяем вызов report() на плейсхолдер - transformed = transformed.replace(fullMatch, placeholder); - } - } - - // === Обработка universal полей === - const universalFields = JSON.parse(cur_frm.doc.universal_fields_storage || "{}"); - - // Создаем словарь соответствия имён полей и их идентификаторов - const fieldNameToId = {}; - Object.keys(universalFields).forEach(fieldId => { - const field = universalFields[fieldId]; - if (field && field.name) { - fieldNameToId[field.name] = fieldId; - } - }); - - // Ищем и заменяем все вызовы univ() в выражении - const univPattern = /univ\(([^)]+)\)/g; - while ((match = univPattern.exec(expression)) !== null) { - const fieldName = match[1]; // Имя поля внутри univ() - const fullMatch = match[0]; // Полное совпадение univ(Name) - - // Ищем ID поля по его имени - const fieldId = fieldNameToId[fieldName]; - - if (fieldId) { - // Заменяем вызов univ() на правильную ссылку с ID поля - transformed = transformed.replace( - fullMatch, - `(frm.doc.__universalFields && frm.doc.__universalFields["${fieldId}"] || 0)` - ); - } else { - // Если поле не найдено, заменяем на 0 и логируем ошибку - console.warn(`Universal field not found: ${fieldName}`); - transformed = transformed.replace(fullMatch, "0"); - } - } - - // === Обработка val() и row() === - const tablePrefixes = ["items", "taxes", "packed_items", "payments", "advances", "timesheets", - "payment_schedule", "sales_team", "aksiz_test_table"]; - - for (const prefix of tablePrefixes) { - const sumPattern = new RegExp(`sum\\(${prefix}_([^)]+)\\)`, 'g'); - transformed = transformed.replace(sumPattern, (m, f) => - `(frm.doc["${prefix}"] && frm.doc["${prefix}"].reduce((s, r) => s + (parseFloat(r["${f}"]) || 0), 0) || 0)` - ); - - const valPattern = new RegExp(`val\\(${prefix}_([^)]+)\\)`, 'g'); - transformed = transformed.replace(valPattern, (m, f) => - `(frm.doc["${prefix}"] && frm.doc["${prefix}"].reduce((s, r) => s + (parseFloat(r["${f}"]) || 0), 0) || 0)` - ); - } - - if (for_child_table) { - transformed = transformed.replace(/val\(([^)]+)\)/g, (m, f) => { - if (f.startsWith(table_name + '_')) { - return `(parseFloat(row["${f.slice(table_name.length + 1)}"]) || 0)`; - } - return `(parseFloat(frm.doc["${f}"]) || 0)`; - }); - - transformed = transformed.replace(/row\(([^)]+)\)/g, (m, f) => - `(parseFloat(row["${f}"]) || 0)` - ); - } else { - transformed = transformed.replace(/val\(([^)]+)\)/g, (m, f) => - `(parseFloat(frm.doc["${f}"]) || 0)` - ); - - transformed = transformed.replace(/row\(([^)]+)\)/g, () => - `0 /* ERROR: row() вне табличной части */` - ); - } - - transformed = transformed.replace(/(frm\.doc\["[^"]+"]|\d+|row\["[^"]+"])\s*([+\-])\s*(\d+(\.\d+)?)%/g, - (match, base, op, percent) => `${base} ${op} ((${base} * ${percent}) / 100)` - ); - - return { transformed, needsAsync, reportCalls }; -} - // Вспомогательная функция для получения доступных префиксов таблиц function getTablePrefixes() { // Предопределенный список известных префиксов таблиц @@ -5459,6 +5519,12 @@ function processAzerbayjanDateField(docValues) { "Dekabr": 12 }; + // Проверяем наличие квартала (rüb) и года (il) + if (docValues && docValues.rüb && docValues.il) { + // Вызываем специальную функцию для обработки квартала + return processQuarterDateField(docValues); + } + // Проверяем, есть ли у нас значения ay и il if (docValues && docValues.ay && monthNames[docValues.ay]) { // Получаем номер месяца @@ -5474,8 +5540,8 @@ function processAzerbayjanDateField(docValues) { const lastDay = new Date(year, monthNum, 0); // Форматируем даты в формате YYYY-MM-DD - const firstDayStr = `${year}.${String(monthNum).padStart(2, '0')}.01`; - const lastDayStr = `${year}.${String(monthNum).padStart(2, '0')}.${String(lastDay.getDate()).padStart(2, '0')}`; + const firstDayStr = `${year}-${String(monthNum).padStart(2, '0')}-01`; + const lastDayStr = `${year}-${String(monthNum).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`; // Возвращаем объект с диапазоном дат return { @@ -5825,7 +5891,6 @@ function openTableRowFormulaModal(frm, formulaId = null) { $modal.find(".modal-overlay").remove(); }) .catch((error) => { - console.error("Ошибка при открытии модального окна формулы строки таблицы:", error); $modal.find(".modal-overlay").remove(); frappe.msgprint({ title: __('Ошибка'), @@ -5953,6 +6018,7 @@ function loadTableFieldsAsync(tableName, $modal, callback) { }); } +// Асинхронная функция для загрузки полей в модальное окно формулы строки таблицы с колбэком // Асинхронная функция для загрузки полей в модальное окно формулы строки таблицы с колбэком function populateTableRowFormulaFieldsAsync(frm, $modal, callback) { const $container = $modal.find(".field-groups-container"); @@ -6042,10 +6108,48 @@ function populateTableRowFormulaFieldsAsync(frm, $modal, callback) { } } - // 4. Добавляем обработчики для кнопок полей - $modal.off('click', '.field-btn, .universal-field-btn').on('click', '.field-btn, .universal-field-btn', function() { + // 4. Добавляем раздел с полями отчетов - ДОБАВЛЯЕМ ЭТОТ БЛОК + if (frm.doc.report_fields_storage) { + const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}"); + + if (Object.keys(reportFields).length > 0) { + const $reportFieldGroup = $(` +
+
Поля отчетов
+
+
+ `); + + const $reportButtonsContainer = $reportFieldGroup.find('.field-buttons-container'); + + Object.keys(reportFields).forEach(fieldId => { + const field = reportFields[fieldId]; + + if (!field || !field.name) return; + + const $fieldBtn = $(` + + `); + + $reportButtonsContainer.append($fieldBtn); + }); + + $container.append($reportFieldGroup); + } + } + + // 5. Добавляем обработчики для кнопок полей + $modal.off('click', '.field-btn, .universal-field-btn, .report-field-btn').on('click', '.field-btn, .universal-field-btn, .report-field-btn', function() { const fieldText = $(this).text().trim(); - const fieldType = $(this).hasClass('universal-field-btn') ? 'universal-field' : 'field'; + const fieldType = $(this).hasClass('universal-field-btn') ? 'universal-field' : + $(this).hasClass('report-field-btn') ? 'report-field' : 'field'; // Добавляем в формулу const $token = $(`${fieldText}×`); @@ -6818,7 +6922,6 @@ function generateTableRowFormulasHandlers(table_row_formulas, functions, target_ try { await calculate_table_row_field(frm, tableName, rowIndex, formula.field_name, formula.formula, "refresh"); } catch (error) { - console.error(\`Error calculating \${formula.field_name} in \${tableName} row \${rowIndex + 1}:\`, error); } }, 0); } else { @@ -6826,7 +6929,6 @@ function generateTableRowFormulasHandlers(table_row_formulas, functions, target_ calculate_table_row_field(frm, tableName, rowIndex, formula.field_name, formula.formula, "refresh"); } } catch (error) { - console.error(\`Error in initialize_table_row_formulas for \${formula.field_name}:\`, error); } }); }); @@ -6840,7 +6942,6 @@ function generateTableRowFormulasHandlers(table_row_formulas, functions, target_ async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, formula, triggerField) { try { if (!frm.doc[tableName] || !frm.doc[tableName][rowIndex]) { - console.warn("Table or row not found:", tableName, rowIndex); return; } @@ -6882,7 +6983,6 @@ async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, fo try { result = eval(finalExpression); } catch (e) { - console.error(\`Ошибка при вычислении выражения строки таблицы: \`, e, finalExpression); } if (isNaN(result)) { @@ -6894,7 +6994,6 @@ async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, fo // Обновляем таблицу frm.refresh_field(tableName); } catch (err) { - console.error(\`Error in calculate_table_row_field for \${fieldName}:\`, err); } } `; @@ -7190,6 +7289,7 @@ function processFormulasForTriggersInClientScript(formula_data, table_row_formul refreshHandlers.push({ field: fieldname, uses_universal: field_info.formula && field_info.formula.includes("univ("), + uses_reports: field_info.formula && field_info.formula.includes("report("), is_table_field: field_info.is_table_field }); } @@ -7245,7 +7345,65 @@ function processFormulasForTriggersInClientScript(formula_data, table_row_formul // Формируем функцию для обработки предопределенных событий и возвращаем её if (refreshHandlers.length > 0 || fieldsUsingUniversal.length > 0) { // Генерируем функцию обработки предопределенных событий и возвращаем её - return generateEventHandlersFunction(refreshHandlers, fieldsUsingUniversal, universal_fields); + let eventHandlerFunction = ` +// Функция для обработки предопределенных событий +async function handle_predefined_events(frm) { + try { + // Сначала вычисляем универсальные поля, если они используются + ${refreshHandlers.some(h => h.uses_universal) || fieldsUsingUniversal.some(f => f.uses_universal_in_formula) ? + 'await calculate_universal_fields(frm);' : + '// Универсальные поля не используются'} + + // Обработка обычных полей с предопределенным событием + ${refreshHandlers.filter(h => !h.is_table_field && h.type !== 'table_row').map(handler => { + if (handler.uses_universal || handler.uses_reports) { + return ` + // Расчет поля ${handler.field} (использует универсальные поля или отчеты) + await calculate_${handler.field}(frm);`; + } else { + return ` + // Расчет поля ${handler.field} + calculate_${handler.field}(frm);`; + } + }).join('')} + + // Обработка строк табличных частей с предопределенным событием + ${refreshHandlers.filter(h => h.type === 'table_row').map(handler => { + // Проверяем, требуется ли асинхронный вызов + const needsAsync = handler.uses_universal || handler.uses_reports; + return ` + // Расчет поля ${handler.field_name} в строке ${handler.row_index} таблицы ${handler.table_name} + try { + ${needsAsync ? 'await ' : ''}calculate_table_row_field(frm, "${handler.table_name}", ${parseInt(handler.row_index) - 1}, "${handler.field_name}", "${handler.formula}", "refresh"); + } catch (error) { + }`; + }).join('')} + + // Обработка полей, использующих универсальные поля в триггерах + ${fieldsUsingUniversal.map(field => { + // Для полей, которые используют универсальные поля в формуле, вызов уже был выше + if (field.uses_universal_in_formula) return ''; + + if (field.is_table_field) { + const parts = field.field.split('_'); + const table_name = parts[0]; + const field_name = parts.slice(1).join('_'); + + return ` + // Обновление поля ${field_name} в таблице ${table_name} (триггер от универсального поля) + update_table_fields(frm, "${table_name}", "${field_name}");`; + } else { + return ` + // Расчет поля ${field.field} (триггер от универсального поля) + calculate_${field.field}(frm);`; + } + }).join('')} + } catch (error) { + console.error("Error in predefined events handler:", error); + } +}`; + + return eventHandlerFunction; } return null; @@ -7284,7 +7442,6 @@ async function handle_predefined_events(frm) { try { ${needsAsync ? 'await ' : ''}calculate_table_row_field(frm, "${handler.table_name}", ${parseInt(handler.row_index) - 1}, "${handler.field_name}", "${handler.formula}", "refresh"); } catch (error) { - console.error("Error calculating table row formula for ${handler.field_name}:", error); }`; }).join('')} @@ -7308,7 +7465,6 @@ async function handle_predefined_events(frm) { } }).join('')} } catch (error) { - console.error("Error in handle_predefined_events:", error); } }`; @@ -8014,30 +8170,29 @@ function openReportFieldModal(frm, fieldId = null, fieldCopy = null) { } }); - // Устанавливаем значения выбранные в выпадающих списках для дат - if (d.filter_controls) { - Object.keys(d.filter_controls).forEach(fieldname => { - const ctrl = d.filter_controls[fieldname]; - if (ctrl && ctrl.df && - (fieldname.toLowerCase().includes('date') || - fieldname.toLowerCase().includes('from') || - fieldname.toLowerCase().includes('to'))) { - - const $select = $(`#filter_${fieldname} select.filter-value`); - if ($select.length) { - const value = $select.val(); - // Если выбрано специальное значение или ссылка на поле документа - if (value === "$AyIlSpecial" || (value && value.startsWith('doc.'))) { - filters[fieldname] = value; - } + // Устанавливаем значения выбранные в выпадающих списках для дат + if (d.filter_controls) { + Object.keys(d.filter_controls).forEach(fieldname => { + const ctrl = d.filter_controls[fieldname]; + if (ctrl && ctrl.df && + (fieldname.toLowerCase().includes('date') || + fieldname.toLowerCase().includes('from') || + fieldname.toLowerCase().includes('to'))) { + + const $select = $(`#filter_${fieldname} select.filter-value`); + if ($select.length) { + const value = $select.val(); + // Если выбрано специальное значение или ссылка на поле документа + if (value === "$AyIlSpecial" || value === "$RubIlSpecial" || (value && value.startsWith('doc.'))) { + filters[fieldname] = value; } } - }); - } + } + }); + } filters_json = JSON.stringify(filters); } catch (e) { - console.error("Ошибка при восстановлении специальных значений:", e); } } @@ -8101,13 +8256,21 @@ function restoreSpecialDateValues(d, current_filters) { return false; // Останавливаем цикл } }); + } else if (originalValue === "$RubIlSpecial") { + // Если это специальное значение квартала, ищем соответствующую опцию + $select.find('option').each(function() { + if ($(this).text().toLowerCase().includes('квартал')) { + $select.val($(this).val()); + return false; // Останавливаем цикл + } + }); } } } } }); } catch (e) { - console.error("Ошибка при восстановлении специальных значений дат:", e); + console.error("Error restoring special date values:", e); } } @@ -8214,26 +8377,28 @@ function initFilterControls(d, currentValues) { }); break; - case 'Date': - // Получаем target_doctype из основной формы - const target_doctype = cur_frm.doc.target_doctype; - - if (target_doctype) { - // Загружаем поля целевого DocType - frappe.model.with_doctype(target_doctype, function() { - const targetMeta = frappe.get_meta(target_doctype); - if (targetMeta) { - // Фильтруем поля даты и специальные поля ay/il - const targetDateFields = targetMeta.fields.filter(field => - field.fieldtype === 'Date' || - field.fieldname === 'ay' || - field.fieldname === 'il' - ); - - // Проверяем, есть ли специальные поля ay и il одновременно - const hasAyField = targetDateFields.some(field => field.fieldname === 'ay'); - const hasIlField = targetDateFields.some(field => field.fieldname === 'il'); - + case 'Date': + // Получаем target_doctype из основной формы + const target_doctype = cur_frm.doc.target_doctype; + + if (target_doctype) { + // Загружаем поля целевого DocType + frappe.model.with_doctype(target_doctype, function() { + const targetMeta = frappe.get_meta(target_doctype); + if (targetMeta) { + // Фильтруем только поля типа Date и специальные поля ay/il/rüb + const targetDateFields = targetMeta.fields.filter(field => + field.fieldtype === 'Date' || + field.fieldname === 'ay' || + field.fieldname === 'il' || + field.fieldname === 'rüb' + ); + + // Проверяем, есть ли специальные поля ay, il и rüb одновременно + const hasAyField = targetDateFields.some(field => field.fieldname === 'ay'); + const hasIlField = targetDateFields.some(field => field.fieldname === 'il'); + const hasRubField = targetDateFields.some(field => field.fieldname === 'rüb'); + if (targetDateFields.length > 0) { // Создаем селект с опциями из полей с датой let selectHtml = ''; @@ -8279,6 +8453,12 @@ function initFilterControls(d, currentValues) { d.set_value('has_special_dates', 1); updateFiltersJson(d); } + else if (selectedValue === "$RubIlSpecial") { + // Помечаем в data-атрибуте, что это специальное поле rüb + $(this).attr('data-is-special-rub-field', 'true'); + d.set_value('has_special_dates', 1); + updateFiltersJson(d); + } else { updateFiltersJson(d); } @@ -8358,7 +8538,6 @@ function initFilterControls(d, currentValues) { // Сохраняем контрол d.filter_controls[filter.fieldname] = control; } catch (e) { - console.error("Ошибка при создании контрола даты:", e); $container.html(''); if (currentValue && !hasOriginalValue) { @@ -8681,7 +8860,6 @@ function saveReportField(frm, values, fieldId = null) { // Обновляем JSON фильтров values.filters_json = JSON.stringify(filters); } catch (e) { - console.error("Ошибка при обработке JSON фильтров:", e); } // Сохраняем информацию о поле отчета @@ -8792,18 +8970,28 @@ function editReportField(frm, fieldId) { Object.keys(filters).forEach(key => { let value = filters[key]; - // Обработка дат + // Обработка дат для месяца (ay) if (key.toLowerCase().includes('date') && value === "$AyIlSpecial") { // Заменяем специальное значение на обычную дату для временного использования // Потом мы восстановим специальное значение при сохранении processedFilters["__original_" + key] = "$AyIlSpecial"; // Сохраняем оригинальное значение processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата - } else if (typeof value === 'string' && value.startsWith("doc.") && + } + // Обработка дат для квартала (rüb) + else if (key.toLowerCase().includes('date') && value === "$RubIlSpecial") { + // Аналогично сохраняем оригинальное значение для квартала + processedFilters["__original_" + key] = "$RubIlSpecial"; // Сохраняем оригинальное значение + processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата + } + // Обработка ссылок на поля документа + else if (typeof value === 'string' && value.startsWith("doc.") && (key.toLowerCase().includes('date') || key.toLowerCase().includes('from') || key.toLowerCase().includes('to'))) { // Обрабатываем также ссылки на поля документа processedFilters["__original_" + key] = value; // Сохраняем оригинальное значение processedFilters[key] = frappe.datetime.get_today(); // Временная валидная дата - } else { + } + // Обычные значения без обработки + else { processedFilters[key] = value; } }); @@ -8812,7 +9000,7 @@ function editReportField(frm, fieldId) { fieldCopy.filters_json = JSON.stringify(processedFilters); fieldCopy.__has_special_dates = true; // Маркер для последующего восстановления } catch (e) { - console.error("Ошибка при обработке JSON фильтров:", e); + console.error("Error processing filters:", e); } } @@ -8898,7 +9086,6 @@ function processTableRowFormulas(table_row_formulas, table_doctypes) { ${usesUniversal || usesReports ? 'await ' : ''}calculate_table_row_field(frm, "${tableName}", rowIndex, "${formula.field_name}", formula, "${trigger}"); } catch (error) { - console.error("Error calculating table row formula for ${formula.field_name}:", error); } }`; @@ -8951,7 +9138,8 @@ function updateFiltersJson(d) { values[fieldname] = value; // Проверка на специальные значения дат - if ((value === "$AyIlSpecial" || (typeof value === 'string' && value.startsWith("doc."))) && + if ((value === "$AyIlSpecial" || value === "$RubIlSpecial" || + (typeof value === 'string' && value.startsWith("doc."))) && (fieldname.toLowerCase().includes('date') || fieldname.toLowerCase().includes('from') || fieldname.toLowerCase().includes('to'))) { @@ -8985,4 +9173,66 @@ function updateFiltersJson(d) { indicator: 'red' }); } +} + +// Функция для обработки квартала (rüb) и года (il) +function processQuarterDateField(docValues) { + // Проверяем, есть ли у нас значения rüb и il + if (docValues && docValues.rüb && docValues.il) { + // Получаем номер квартала (преобразуем из строки вида "1 Rüb" в число 1) + const quarterStr = docValues.rüb; + const quarterMatch = quarterStr.match(/(\d+)/); + + if (!quarterMatch) { + return null; + } + + const quarterNum = parseInt(quarterMatch[1]); + + // Получаем год из поля il или используем текущий год + let year = docValues.il || new Date().getFullYear(); + + // Определяем начальный и конечный месяц квартала + let startMonth, endMonth; + + switch(quarterNum) { + case 1: + startMonth = 1; // Январь + endMonth = 3; // Март + break; + case 2: + startMonth = 4; // Апрель + endMonth = 6; // Июнь + break; + case 3: + startMonth = 7; // Июль + endMonth = 9; // Сентябрь + break; + case 4: + startMonth = 10; // Октябрь + endMonth = 12; // Декабрь + break; + default: + return null; + } + + // Определяем первый день квартала (1-е число начального месяца) + const firstDay = new Date(year, startMonth - 1, 1); + + // Последний день квартала (последнее число конечного месяца) + const lastDay = new Date(year, endMonth, 0); + + // Форматируем даты в формате YYYY-MM-DD + const firstDayStr = `${year}-${String(startMonth).padStart(2, '0')}-01`; + const lastDayStr = `${year}-${String(endMonth).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`; + + // Возвращаем объект с диапазоном дат + return { + start: firstDayStr, + end: lastDayStr + }; + } + + // Если это не специальное поле или значение не распознано, возвращаем null + return null; } \ No newline at end of file diff --git a/formula_editor/formula_editor/doctype/report_simulator/report_simulator.json b/formula_editor/formula_editor/doctype/report_simulator/report_simulator.json index 0f6ba05..38662a2 100644 --- a/formula_editor/formula_editor/doctype/report_simulator/report_simulator.json +++ b/formula_editor/formula_editor/doctype/report_simulator/report_simulator.json @@ -36,7 +36,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-04-08 17:10:17.242850", + "modified": "2025-04-18 18:47:01.288110", "modified_by": "Administrator", "module": "Formula Editor", "name": "Report Simulator",