refactor(generator): emit config, not a code-generated engine
The generator unrolled the whole engine into the Client Script: one inline block per formula per trigger, plus an if-chain over every report field. A VAT declaration came out at 403 KB of generated code, which then shipped inside taxes_az's fixtures. It now emits a CONFIG object — a copy of the storage — and one call into taxes_az.formula. The storage format is untouched, so every existing Formula Editor record keeps working. Generated scripts no longer reference this app, which is what lets it be removed from production machines. Generation moved from the browser to on_update. It used to run in before_save off the editor's rendered DOM, so a half-drawn form could serialise empty formulas over real ones; it now builds from the stored JSON. Removed with it: the emitters (transform_expression, table-row handlers, universal fields, report fields), plus dead code they hid — processFormula had no call sites, applyAggregation was never emitted, and generate_child_table_script referenced a variable outside its closure and would have thrown on the first is_table_field formula. VAT script: 403 KB -> 120 KB. Output verified identical across all 426 formulas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe272c3c79
commit
bb1287acc2
|
|
@ -483,100 +483,133 @@ def on_trash(doc, method=None):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Генерируем функцию для работы с отчетами для клиентского скрипта
|
|
||||||
def generateReportFieldsFunction(report_fields):
|
|
||||||
"""Генерирует JavaScript функцию для работы с полями отчетов"""
|
|
||||||
if not report_fields:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
return f"""
|
# --- Client Script generation -------------------------------------------------
|
||||||
async function getReportFieldValue(frm, reportFieldName) {{
|
#
|
||||||
try {{
|
# The generated script is what actually runs on a declaration form. It used to be
|
||||||
// Получаем информацию о поле отчета
|
# built in the browser from the editor's rendered DOM, which meant a half-drawn
|
||||||
const reportFields = {json.dumps(report_fields)};
|
# form could serialise empty formulas over good ones. It is built here from the
|
||||||
const fieldInfo = reportFields[reportFieldName];
|
# stored JSON instead, so saving a Formula Editor record always regenerates the
|
||||||
|
# script from the data of record.
|
||||||
if (!fieldInfo) {{
|
#
|
||||||
console.warn('Report field not found:', reportFieldName);
|
# The emitted script carries only a CONFIG object and a call into taxes_az.formula.
|
||||||
return 0;
|
# It contains no reference to this app, so formula_editor can be uninstalled from
|
||||||
}}
|
# production machines and the declarations keep calculating.
|
||||||
|
|
||||||
// Парсим фильтры
|
CLIENT_SCRIPT_PREFIX = "Formula_Editor_Script_for_"
|
||||||
let filters = {{}};
|
|
||||||
try {{
|
# Client Scripts ship to production through taxes_az's fixtures, which select on
|
||||||
filters = JSON.parse(fieldInfo.filters_json || '{{}}');
|
# this module.
|
||||||
}} catch (e) {{
|
CLIENT_SCRIPT_MODULE = "Taxes Az"
|
||||||
console.error('Error parsing filters JSON:', e);
|
|
||||||
return 0;
|
SCRIPT_TEMPLATE = """// Generated by Formula Editor
|
||||||
}}
|
// Hesablamanı taxes_az.formula aparır (bax: taxes_az/public/js/formula_runtime.js).
|
||||||
|
|
||||||
// Обрабатываем фильтры с возможными ссылками на поля документа
|
const CONFIG = {config};
|
||||||
const processedFilters = {{}};
|
|
||||||
|
frappe.ui.form.on({doctype}, taxes_az.formula.handlers(CONFIG));
|
||||||
for (const key in filters) {{
|
"""
|
||||||
let value = filters[key];
|
|
||||||
|
|
||||||
// Проверяем, является ли значение ссылкой на поле документа
|
def build_config(doc):
|
||||||
if (typeof value === 'string' && value.startsWith('doc.')) {{
|
"""Project a Formula Editor record onto the config its runtime consumes.
|
||||||
const docField = value.substring(4);
|
|
||||||
if (frm.doc[docField] !== undefined) {{
|
This is a copy of the stored JSON, not a new format — the storage fields are
|
||||||
processedFilters[key] = frm.doc[docField];
|
the contract and are passed through untouched.
|
||||||
continue;
|
"""
|
||||||
}}
|
return {
|
||||||
}}
|
"doctype": doc.target_doctype,
|
||||||
|
"formulas": json.loads(doc.formula_storage or "{}"),
|
||||||
// Обычные значения
|
"table_row_formulas": json.loads(doc.table_row_formulas_storage or "{}"),
|
||||||
processedFilters[key] = value;
|
"report_fields": normalize_report_filters(json.loads(doc.report_fields_storage or "{}")),
|
||||||
}}
|
"universal_fields": json.loads(doc.universal_fields_storage or "{}"),
|
||||||
|
}
|
||||||
// Проверяем кеш
|
|
||||||
if (!window.reportCache) window.reportCache = {{}};
|
|
||||||
|
def normalize_report_filters(report_fields):
|
||||||
const cacheKey = `${{fieldInfo.report}}|${{JSON.stringify(processedFilters)}}|${{fieldInfo.field_name}}`;
|
"""Force report filters onto the placeholders the runtime knows how to resolve.
|
||||||
const now = new Date().getTime();
|
|
||||||
|
`company` becomes $DefaultCompany and anything date-shaped becomes $AyIlSpecial,
|
||||||
if (window.reportCache[cacheKey] &&
|
exactly as the previous generator did while baking filters into the script. This
|
||||||
window.reportCache[cacheKey].timestamp > now - 5 * 60 * 1000) {{
|
is not cosmetic: a different filter value means the report returns different
|
||||||
return window.reportCache[cacheKey].value;
|
figures, and those figures go on a tax return.
|
||||||
}}
|
|
||||||
|
Operates on the parsed copy; the stored `filters_json` is never rewritten.
|
||||||
// Выполняем запрос к серверу
|
"""
|
||||||
try {{
|
for field in report_fields.values():
|
||||||
const result = await frappe.call({{
|
if not field:
|
||||||
method: "formula_editor.api.run_report_for_formula",
|
continue
|
||||||
args: {{
|
|
||||||
report_name: fieldInfo.report,
|
try:
|
||||||
filters: processedFilters,
|
filters = json.loads(field.get("filters_json") or "{}")
|
||||||
field_name: fieldInfo.field_name
|
except ValueError:
|
||||||
}},
|
field["filters_json"] = "{}"
|
||||||
freeze: false
|
continue
|
||||||
}});
|
|
||||||
|
company = filters.get("company")
|
||||||
if (result && result.message !== undefined) {{
|
if isinstance(company, str) and not company.startswith("$"):
|
||||||
if (result.message.error) {{
|
filters["company"] = "$DefaultCompany"
|
||||||
console.error('Report error:', result.message.error);
|
|
||||||
return 0;
|
for key in list(filters):
|
||||||
}}
|
lowered = key.lower()
|
||||||
|
if not ("date" in lowered or "from" in lowered or "to" in lowered):
|
||||||
let value = parseFloat(result.message);
|
continue
|
||||||
if (isNaN(value)) value = 0;
|
|
||||||
|
value = filters[key]
|
||||||
// Сохраняем в кеш
|
if not isinstance(value, str):
|
||||||
window.reportCache[cacheKey] = {{
|
continue
|
||||||
value: value,
|
if value in ("$AyIlSpecial", "$RubIlSpecial") or value.startswith("doc."):
|
||||||
timestamp: now
|
continue
|
||||||
}};
|
|
||||||
|
filters[key] = "$AyIlSpecial"
|
||||||
return value;
|
|
||||||
}}
|
field["filters_json"] = json.dumps(filters)
|
||||||
}} catch (err) {{
|
|
||||||
console.error('Report field error:', err);
|
return report_fields
|
||||||
}}
|
|
||||||
|
|
||||||
return 0;
|
def regenerate_client_script(doc, method=None):
|
||||||
}} catch (error) {{
|
"""Rebuild the Client Script for a Formula Editor record. Bound to on_update."""
|
||||||
console.error('getReportFieldValue error:', error);
|
if not doc.target_doctype:
|
||||||
return 0;
|
return
|
||||||
}}
|
|
||||||
}}
|
script = SCRIPT_TEMPLATE.format(
|
||||||
"""
|
config=json.dumps(build_config(doc), ensure_ascii=False, indent=1),
|
||||||
|
doctype=json.dumps(doc.target_doctype, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
name = CLIENT_SCRIPT_PREFIX + doc.target_doctype
|
||||||
|
|
||||||
|
if frappe.db.exists("Client Script", name):
|
||||||
|
client_script = frappe.get_doc("Client Script", name)
|
||||||
|
client_script.dt = doc.target_doctype
|
||||||
|
client_script.view = "Form"
|
||||||
|
client_script.enabled = 1
|
||||||
|
client_script.script = script
|
||||||
|
client_script.save(ignore_permissions=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Client Script",
|
||||||
|
"name": name,
|
||||||
|
"dt": doc.target_doctype,
|
||||||
|
"view": "Form",
|
||||||
|
"script_type": "Client",
|
||||||
|
"module": CLIENT_SCRIPT_MODULE,
|
||||||
|
"enabled": 1,
|
||||||
|
"script": script,
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def regenerate_all():
|
||||||
|
"""Regenerate every declaration's Client Script from its Formula Editor record."""
|
||||||
|
regenerated = []
|
||||||
|
for name in frappe.get_all("Formula Editor", pluck="name"):
|
||||||
|
doc = frappe.get_doc("Formula Editor", name)
|
||||||
|
regenerate_client_script(doc)
|
||||||
|
regenerated.append(doc.target_doctype)
|
||||||
|
|
||||||
|
frappe.db.commit()
|
||||||
|
return regenerated
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,9 @@ frappe.require([
|
||||||
},
|
},
|
||||||
|
|
||||||
before_save: function(frm) {
|
before_save: function(frm) {
|
||||||
|
// Скрипт генерирует сервер в on_update (formula_editor.api), из
|
||||||
|
// сохранённого storage — здесь только переносим формулы из UI в него.
|
||||||
save_formulas(frm);
|
save_formulas(frm);
|
||||||
generate_client_script(frm);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
before_remove: function(frm) {
|
before_remove: function(frm) {
|
||||||
|
|
@ -203,496 +204,6 @@ frappe.require([
|
||||||
return 10; // значение по умолчанию
|
return 10; // значение по умолчанию
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерация клиентского скрипта
|
|
||||||
function generate_client_script(frm) {
|
|
||||||
if (!frm.doc.target_doctype) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let target_doctype = frm.doc.target_doctype;
|
|
||||||
let formula_data = JSON.parse(frm.doc.formula_storage || "{}");
|
|
||||||
let universal_fields = JSON.parse(frm.doc.universal_fields_storage || "{}");
|
|
||||||
let table_row_formulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}");
|
|
||||||
let report_fields = JSON.parse(frm.doc.report_fields_storage || "{}");
|
|
||||||
|
|
||||||
let main_script = `// Generated by Formula Editor\nfrappe.ui.form.on("${target_doctype}", {\n`;
|
|
||||||
let functions = [];
|
|
||||||
let table_child_scripts = {};
|
|
||||||
let table_doctypes = {};
|
|
||||||
let refreshHandlers = []; // Добавим это для хранения обработчиков refresh
|
|
||||||
|
|
||||||
// Хранилище для объединения триггеров
|
|
||||||
let triggerHandlers = {};
|
|
||||||
|
|
||||||
// Собираем информацию о табличных триггерах - ключ: table_field, значение: массив полей основного доктайпа
|
|
||||||
const table_field_triggers = {};
|
|
||||||
|
|
||||||
// Проходим по всем формулам, ищем триггеры вида tablename_fieldname
|
|
||||||
Object.keys(formula_data).forEach(fieldname => {
|
|
||||||
let field_info = formula_data[fieldname];
|
|
||||||
if (!field_info.is_table_field && field_info.triggers) {
|
|
||||||
let triggers = field_info.triggers.split(',').map(t => t.trim());
|
|
||||||
|
|
||||||
triggers.forEach(trigger => {
|
|
||||||
if (trigger.includes('_')) {
|
|
||||||
const parts = trigger.split('_');
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
const tableName = parts[0];
|
|
||||||
const fieldName = parts.slice(1).join('_');
|
|
||||||
|
|
||||||
// Добавляем информацию о триггере
|
|
||||||
if (!table_field_triggers[trigger]) {
|
|
||||||
table_field_triggers[trigger] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем поле основного документа и признак асинхронности
|
|
||||||
table_field_triggers[trigger].push({
|
|
||||||
main_field: fieldname,
|
|
||||||
uses_universal: field_info.formula.includes("univ("),
|
|
||||||
uses_reports: field_info.formula.includes("report(")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Собираем все триггеры из табличных частей для последующей фильтрации
|
|
||||||
const tableTriggersSet = new Set();
|
|
||||||
Object.keys(table_field_triggers).forEach(trigger => {
|
|
||||||
tableTriggersSet.add(trigger);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получаем информацию о таблицах DocType
|
|
||||||
getTableDoctypes(target_doctype, table_doctypes);
|
|
||||||
|
|
||||||
// Сбор всех табличных частей и их полей
|
|
||||||
let child_tables = {};
|
|
||||||
collectChildTables(formula_data, child_tables);
|
|
||||||
|
|
||||||
// Добавляем функцию для обработки и выполнения формул
|
|
||||||
functions.push(`
|
|
||||||
// Функция для обработки и выполнения формул
|
|
||||||
async function processFormula(frm, formula) {
|
|
||||||
try {
|
|
||||||
// Проверяем наличие вызовов отчетов
|
|
||||||
if (formula.includes("report(")) {
|
|
||||||
// Извлекаем все вызовы report() из формулы
|
|
||||||
const reportRegex = /report\\(([^)]+)\\)/g;
|
|
||||||
let match;
|
|
||||||
let processedFormula = formula;
|
|
||||||
|
|
||||||
// Массив для хранения промисов запросов к отчетам
|
|
||||||
const reportPromises = [];
|
|
||||||
const reportPlaceholders = [];
|
|
||||||
|
|
||||||
// Находим все вызовы report() и заменяем их на плейсхолдеры
|
|
||||||
while ((match = reportRegex.exec(formula)) !== null) {
|
|
||||||
const fullMatch = match[0]; // report(fieldName)
|
|
||||||
const fieldName = match[1]; // fieldName
|
|
||||||
|
|
||||||
// Создаем уникальный плейсхолдер
|
|
||||||
const placeholder = \`__REPORT_PLACEHOLDER_\${reportPromises.length}__\`;
|
|
||||||
|
|
||||||
// Ищем данные отчета в хранилище
|
|
||||||
const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
|
|
||||||
const reportField = Object.values(reportFields).find(f => f.name === fieldName);
|
|
||||||
|
|
||||||
if (reportField) {
|
|
||||||
// Добавляем промис запроса к отчету
|
|
||||||
reportPromises.push(
|
|
||||||
getReportFieldValue(
|
|
||||||
frm,
|
|
||||||
reportField.report,
|
|
||||||
JSON.parse(reportField.filters_json || "{}"),
|
|
||||||
reportField.field_name,
|
|
||||||
reportField.aggregation || 'sum'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Запоминаем соответствие плейсхолдера и вызова
|
|
||||||
reportPlaceholders.push({
|
|
||||||
placeholder: placeholder,
|
|
||||||
original: fullMatch
|
|
||||||
});
|
|
||||||
|
|
||||||
// Заменяем вызов отчета на плейсхолдер
|
|
||||||
processedFormula = processedFormula.replace(fullMatch, placeholder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если нашли вызовы отчетов, ждем выполнения всех запросов
|
|
||||||
if (reportPromises.length > 0) {
|
|
||||||
const reportResults = await Promise.all(reportPromises);
|
|
||||||
|
|
||||||
// Заменяем плейсхолдеры на результаты
|
|
||||||
for (let i = 0; i < reportPlaceholders.length; i++) {
|
|
||||||
// Убедимся, что результат - числовое значение
|
|
||||||
const reportValue = reportResults[i] !== undefined ? Number(reportResults[i]) : 0;
|
|
||||||
processedFormula = processedFormula.replace(
|
|
||||||
reportPlaceholders[i].placeholder,
|
|
||||||
reportValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Теперь формула не содержит вызовов report(), обрабатываем её обычным способом
|
|
||||||
let transformResult = transform_expression(processedFormula, false);
|
|
||||||
|
|
||||||
// Если есть внешняя трансформация, применяем её
|
|
||||||
let result;
|
|
||||||
if (transformResult && transformResult.transformed) {
|
|
||||||
result = eval(transformResult.transformed);
|
|
||||||
} else {
|
|
||||||
// Иначе просто вычисляем выражение
|
|
||||||
result = eval(processedFormula);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Возвращаем результат
|
|
||||||
return isNaN(result) ? 0 : result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Стандартная обработка для формул без отчетов
|
|
||||||
let transformResult = transform_expression(formula, false);
|
|
||||||
let result = eval(transformResult.transformed);
|
|
||||||
return isNaN(result) ? 0 : result;
|
|
||||||
} catch (e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}`);
|
|
||||||
|
|
||||||
// Добавляем функцию для расчета универсальных полей
|
|
||||||
functions.push(generateUniversalFieldsFunction(universal_fields));
|
|
||||||
|
|
||||||
// Добавляем функцию для получения значений полей отчетов
|
|
||||||
if (Object.keys(report_fields).length > 0) {
|
|
||||||
functions.push(generateReportFieldsFunction(report_fields));
|
|
||||||
}
|
|
||||||
|
|
||||||
// НЕ добавляем обработчики для ay и il здесь отдельно
|
|
||||||
// Они будут добавлены позже вместе с другими обработчиками
|
|
||||||
|
|
||||||
// Добавляем функцию преобразования выражений
|
|
||||||
functions.push(generateTransformExpressionFunction(universal_fields, report_fields, table_doctypes));
|
|
||||||
// Добавляем функцию для работы с конкретными строками табличных частей
|
|
||||||
functions.push(generateTableRowAccessFunction());
|
|
||||||
|
|
||||||
// Создаем список всех полей, которые используют универсальные поля или отчеты
|
|
||||||
let fieldsUsingUniversalOrReports = [];
|
|
||||||
|
|
||||||
// Проверяем, какие поля используют универсальные поля или отчеты
|
|
||||||
Object.keys(formula_data).forEach(fieldname => {
|
|
||||||
let field_info = formula_data[fieldname];
|
|
||||||
if (field_info && field_info.formula) {
|
|
||||||
if (field_info.formula.includes("univ(")) {
|
|
||||||
fieldsUsingUniversalOrReports.push({
|
|
||||||
field: fieldname,
|
|
||||||
uses_universal: true,
|
|
||||||
uses_reports: field_info.formula.includes("report(")
|
|
||||||
});
|
|
||||||
} else if (field_info.formula.includes("report(")) {
|
|
||||||
fieldsUsingUniversalOrReports.push({
|
|
||||||
field: fieldname,
|
|
||||||
uses_universal: false,
|
|
||||||
uses_reports: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обрабатываем все формулы для основного документа
|
|
||||||
Object.keys(formula_data).forEach(fieldname => {
|
|
||||||
let field_info = formula_data[fieldname];
|
|
||||||
|
|
||||||
if (!field_info || typeof field_info !== "object" || !field_info.formula) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем функцию расчета для поля
|
|
||||||
let is_table_field = field_info.is_table_field;
|
|
||||||
let formula = field_info.formula.trim();
|
|
||||||
|
|
||||||
// Все функции расчета делаем асинхронными для единообразия,
|
|
||||||
// так как мы не можем точно определить все случаи использования report()
|
|
||||||
if (is_table_field) {
|
|
||||||
// Обработка полей табличных частей...
|
|
||||||
let parts = fieldname.split('_');
|
|
||||||
let table_name = parts[0];
|
|
||||||
let field_name_in_table = parts.slice(1).join('_');
|
|
||||||
|
|
||||||
if (!table_child_scripts[table_name]) {
|
|
||||||
table_child_scripts[table_name] = {
|
|
||||||
doctype: table_doctypes[table_name] || `${target_doctype} Item`,
|
|
||||||
calculations: [],
|
|
||||||
fields: new Set(),
|
|
||||||
uses_universal: false,
|
|
||||||
uses_reports: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Определяем, нужна ли асинхронная функция
|
|
||||||
let uses_universal = formula.includes("univ(");
|
|
||||||
let uses_reports = formula.includes("report(");
|
|
||||||
|
|
||||||
table_child_scripts[table_name].calculations.push({
|
|
||||||
field: field_name_in_table,
|
|
||||||
formula: formula,
|
|
||||||
uses_universal: uses_universal,
|
|
||||||
uses_reports: uses_reports
|
|
||||||
});
|
|
||||||
|
|
||||||
// Помечаем таблицу, если хотя бы одно поле использует универсальные поля или отчеты
|
|
||||||
if (uses_universal) {
|
|
||||||
table_child_scripts[table_name].uses_universal = true;
|
|
||||||
}
|
|
||||||
if (uses_reports) {
|
|
||||||
table_child_scripts[table_name].uses_reports = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field_info.triggers) {
|
|
||||||
field_info.triggers.split(',').map(t => t.trim()).forEach(trigger => {
|
|
||||||
table_child_scripts[table_name].fields.add(trigger);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Используем единый подход для всех формул через transform_expression
|
|
||||||
functions.push(`
|
|
||||||
async function calculate_${fieldname}(frm) {
|
|
||||||
try {
|
|
||||||
${formula.includes("univ(") ? 'await calculate_universal_fields(frm);' : ''}
|
|
||||||
const { transformed, needsAsync, reportCalls } = transform_expression(${JSON.stringify(formula)}, false);
|
|
||||||
|
|
||||||
// Обработка вызовов отчетов, если они есть
|
|
||||||
let finalExpression = transformed;
|
|
||||||
if (reportCalls && reportCalls.length > 0) {
|
|
||||||
// Собираем все результаты отчетов
|
|
||||||
const reportResults = [];
|
|
||||||
for (const call of reportCalls) {
|
|
||||||
const result = await getReportFieldValue(
|
|
||||||
frm,
|
|
||||||
call.reportName,
|
|
||||||
call.filters,
|
|
||||||
call.fieldName,
|
|
||||||
call.aggregation
|
|
||||||
);
|
|
||||||
reportResults.push({ placeholder: call.placeholder, value: result });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Заменяем плейсхолдеры на полученные значения
|
|
||||||
for (const result of reportResults) {
|
|
||||||
finalExpression = finalExpression.replace(result.placeholder, result.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Теперь можно безопасно вычислить выражение
|
|
||||||
const result = eval(finalExpression);
|
|
||||||
frm.set_value("${fieldname}", isNaN(result) ? 0 : result);
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
}`);
|
|
||||||
|
|
||||||
// Проверяем триггеры
|
|
||||||
if (field_info.triggers) {
|
|
||||||
let triggers = field_info.triggers.split(',').map(t => t.trim());
|
|
||||||
|
|
||||||
// Проверяем наличие предопределенного события "При открытии документа"
|
|
||||||
const hasRefreshTrigger = triggers.some(trigger => isPredefinedEvent(trigger));
|
|
||||||
|
|
||||||
if (hasRefreshTrigger) {
|
|
||||||
// Добавляем в список обработчиков refresh
|
|
||||||
refreshHandlers.push({
|
|
||||||
field: fieldname,
|
|
||||||
uses_universal: formula.includes("univ("),
|
|
||||||
uses_reports: formula.includes("report(")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Удаляем предопределенное событие из списка триггеров
|
|
||||||
triggers = triggers.filter(trigger => !isPredefinedEvent(trigger));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Фильтруем триггеры, исключая те, что уже добавлены в table_field_triggers
|
|
||||||
const filteredTriggers = triggers.filter(trigger => !tableTriggersSet.has(trigger));
|
|
||||||
|
|
||||||
// Удаляем дубликаты триггеров
|
|
||||||
const uniqueTriggers = [...new Set(filteredTriggers)];
|
|
||||||
|
|
||||||
uniqueTriggers.forEach(trigger => {
|
|
||||||
if (trigger) {
|
|
||||||
// Добавляем обработчик в хранилище триггеров
|
|
||||||
if (!triggerHandlers[trigger]) {
|
|
||||||
triggerHandlers[trigger] = {
|
|
||||||
isAsync: true, // ✅ ВСЕГДА true, т.к. функции calculate_* всегда async
|
|
||||||
handlers: []
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Убедимся что async = true
|
|
||||||
triggerHandlers[trigger].isAsync = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем вызов функции расчета
|
|
||||||
triggerHandlers[trigger].handlers.push(`await calculate_${fieldname}(frm); // order: ${field_info.order || 10}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем функции для расчета формул конкретных строк табличных частей
|
|
||||||
if (Object.keys(table_row_formulas).length > 0) {
|
|
||||||
// Создаем функции для расчета формул строк таблиц
|
|
||||||
generateTableRowFormulasHandlers(table_row_formulas, functions, target_doctype, universal_fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем обработчик refresh, если были найдены формулы с триггером "При открытии документа"
|
|
||||||
if (refreshHandlers.length > 0 || Object.keys(table_row_formulas).length > 0) {
|
|
||||||
// Определяем, нужна ли асинхронная функция для refresh
|
|
||||||
const needsAsync = refreshHandlers.some(h => h.uses_universal || h.uses_reports) ||
|
|
||||||
Object.keys(table_row_formulas).some(id => {
|
|
||||||
const formula = table_row_formulas[id];
|
|
||||||
return formula.formula.includes("univ(") || formula.formula.includes("report(");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создаем обработчик refresh с вызовом всех нужных функций расчета
|
|
||||||
let refreshHandlerBody = ` "refresh": ${needsAsync ? 'async ' : ''}function(frm) {\n`;
|
|
||||||
|
|
||||||
// Если есть формулы для строк табличных частей, инициализируем их
|
|
||||||
if (Object.keys(table_row_formulas).length > 0) {
|
|
||||||
refreshHandlerBody += ` initialize_table_row_formulas(frm);\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем вызов расчета универсальных полей, если они используются
|
|
||||||
if (refreshHandlers.some(h => h.uses_universal)) {
|
|
||||||
refreshHandlerBody += ` await calculate_universal_fields(frm);\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сортируем обработчики refresh по порядку
|
|
||||||
const sortedRefreshHandlers = refreshHandlers.slice().sort((a, b) => {
|
|
||||||
const orderA = formula_data[a.field] ? (formula_data[a.field].order || 10) : 10;
|
|
||||||
const orderB = formula_data[b.field] ? (formula_data[b.field].order || 10) : 10;
|
|
||||||
return orderA - orderB;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем вызовы для каждого поля с триггером "При открытии документа"
|
|
||||||
sortedRefreshHandlers.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 в начало скрипта
|
|
||||||
main_script = main_script.replace('frappe.ui.form.on("' + target_doctype + '", {\n',
|
|
||||||
'frappe.ui.form.on("' + target_doctype + '", {\n' + refreshHandlerBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчики для табличных частей и триггеры, связанные с ними
|
|
||||||
const tableRowFormulaHandlers = processTableRowFormulas(table_row_formulas, table_doctypes);
|
|
||||||
|
|
||||||
// ВАЖНО: Объединяем обработчики формул строк таблиц с основными обработчиками
|
|
||||||
Object.keys(tableRowFormulaHandlers).forEach(trigger => {
|
|
||||||
const handler = tableRowFormulaHandlers[trigger];
|
|
||||||
|
|
||||||
// Проверяем, есть ли уже обработчик для этого триггера
|
|
||||||
if (triggerHandlers[trigger]) {
|
|
||||||
// Объединяем обработчики
|
|
||||||
// Если хотя бы один требует async, делаем весь обработчик async
|
|
||||||
if (handler.isAsync) {
|
|
||||||
triggerHandlers[trigger].isAsync = true;
|
|
||||||
}
|
|
||||||
// Добавляем новые обработчики к существующим
|
|
||||||
triggerHandlers[trigger].handlers = triggerHandlers[trigger].handlers.concat(handler.handlers);
|
|
||||||
} else {
|
|
||||||
// Если обработчика еще нет, создаем новый
|
|
||||||
triggerHandlers[trigger] = handler;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверяем, нужны ли обработчики для ay и il для универсальных полей и основных формул
|
|
||||||
const needsAyIlHandlers = fieldsUsingUniversalOrReports.some(f => f.uses_universal) ||
|
|
||||||
Object.keys(table_row_formulas).some(id => {
|
|
||||||
const formula = table_row_formulas[id];
|
|
||||||
return formula.formula && formula.formula.includes("univ(");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (needsAyIlHandlers) {
|
|
||||||
// Добавляем или обновляем обработчики для ay и il
|
|
||||||
['ay', 'il'].forEach(field => {
|
|
||||||
if (!triggerHandlers[field]) {
|
|
||||||
triggerHandlers[field] = {
|
|
||||||
isAsync: true,
|
|
||||||
handlers: []
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
triggerHandlers[field].isAsync = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем вызов calculate_universal_fields в начало обработчика
|
|
||||||
const hasUniversalCall = triggerHandlers[field].handlers.some(h =>
|
|
||||||
h.includes('calculate_universal_fields')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasUniversalCall) {
|
|
||||||
triggerHandlers[field].handlers.unshift('await calculate_universal_fields(frm); // order: 0');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Теперь создаем обработчики для каждого триггера (без дубликатов)
|
|
||||||
Object.keys(triggerHandlers).forEach(trigger => {
|
|
||||||
const handler = triggerHandlers[trigger];
|
|
||||||
main_script += ` "${trigger}": ${handler.isAsync ? 'async ' : ''}function(frm) {\n`;
|
|
||||||
|
|
||||||
// Для ay и il добавляем комментарий о вычислении универсальных полей
|
|
||||||
if (trigger === 'ay' || trigger === 'il') {
|
|
||||||
if (handler.handlers.some(h => h.includes('calculate_universal_fields'))) {
|
|
||||||
main_script += ` // Вычисляем универсальные поля только когда меняется ${trigger}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сортируем обработчики по порядку выполнения
|
|
||||||
const sortedHandlers = handler.handlers.slice().sort((a, b) => {
|
|
||||||
// Извлекаем порядок из строки обработчика
|
|
||||||
const orderA = extractOrderFromHandler(a) || 10;
|
|
||||||
const orderB = extractOrderFromHandler(b) || 10;
|
|
||||||
return orderA - orderB;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем отсортированные обработчики
|
|
||||||
sortedHandlers.forEach(handlerCode => {
|
|
||||||
main_script += ` ${handlerCode}\n`;
|
|
||||||
});
|
|
||||||
|
|
||||||
main_script += ' },\n';
|
|
||||||
});
|
|
||||||
|
|
||||||
main_script += `});`;
|
|
||||||
|
|
||||||
// Напрямую добавляем скрипты для дочерних таблиц
|
|
||||||
const child_scripts = generate_child_table_script(table_child_scripts, table_doctypes, table_field_triggers, formula_data, functions);
|
|
||||||
|
|
||||||
// Добавляем скрипты дочерних таблиц к основному скрипту
|
|
||||||
main_script += child_scripts;
|
|
||||||
|
|
||||||
let final_script = `${main_script}\n\n${functions.join("\n\n")}`;
|
|
||||||
update_or_create_client_script(target_doctype, final_script);
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI компоненты - создание и обработка полей
|
// UI компоненты - создание и обработка полей
|
||||||
function generate_dynamic_fields(frm, doctype, callback) {
|
function generate_dynamic_fields(frm, doctype, callback) {
|
||||||
// Проверяем доступность зависимых функций
|
// Проверяем доступность зависимых функций
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,10 @@ fixtures = [
|
||||||
|
|
||||||
doc_events = {
|
doc_events = {
|
||||||
"Formula Editor": {
|
"Formula Editor": {
|
||||||
|
# Скрипт декларации строится из storage на сервере, а не из вёрстки
|
||||||
|
# редактора: полуотрисованная форма раньше могла записать пустые формулы
|
||||||
|
# поверх настоящих.
|
||||||
|
"on_update": "formula_editor.api.regenerate_client_script",
|
||||||
"on_trash": "formula_editor.api.on_trash"
|
"on_trash": "formula_editor.api.on_trash"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,6 @@
|
||||||
function update_or_create_client_script(target_doctype, script_code) {
|
// Единственное, что осталось от старого генератора кода: удаление скрипта, когда
|
||||||
// Имя для нашего клиентского скрипта формул
|
// запись Formula Editor удаляют из UI (before_remove). Сборку скрипта делает
|
||||||
const formula_script_name = `Formula_Editor_Script_for_${target_doctype}`;
|
// сервер — formula_editor.api.regenerate_client_script, по on_update.
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "frappe.client.get_list",
|
|
||||||
args: {
|
|
||||||
doctype: "Client Script",
|
|
||||||
filters: { dt: target_doctype },
|
|
||||||
fields: ["name", "script"]
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message && r.message.length > 0) {
|
|
||||||
// Находим наш скрипт по имени или по содержимому
|
|
||||||
let formula_script = r.message.find(script =>
|
|
||||||
script.name === formula_script_name ||
|
|
||||||
(script.script && script.script.includes('// Generated by Formula Editor'))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (formula_script) {
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "frappe.client.set_value",
|
|
||||||
args: {
|
|
||||||
doctype: "Client Script",
|
|
||||||
name: formula_script.name,
|
|
||||||
fieldname: {
|
|
||||||
script: script_code,
|
|
||||||
// Обновляем имя, если оно было другим
|
|
||||||
name: formula_script_name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
callback: function(response) {
|
|
||||||
if (response.message) {
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
createNewFormulaScript(target_doctype, script_code, formula_script_name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
createNewFormulaScript(target_doctype, script_code, formula_script_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewFormulaScript(target_doctype, script_code, script_name) {
|
|
||||||
// Добавляем комментарий для идентификации скрипта в будущем
|
|
||||||
const marked_script_code = `// Generated by Formula Editor\n${script_code}`;
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "frappe.client.insert",
|
|
||||||
args: {
|
|
||||||
doc: {
|
|
||||||
doctype: "Client Script",
|
|
||||||
name: script_name,
|
|
||||||
dt: target_doctype,
|
|
||||||
script_type: "Client",
|
|
||||||
script: marked_script_code,
|
|
||||||
enabled: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
callback: function(response) {
|
|
||||||
if (response.message) {
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function delete_client_script_for_doctype(doctype) {
|
function delete_client_script_for_doctype(doctype) {
|
||||||
if (!doctype) {
|
if (!doctype) {
|
||||||
|
|
@ -107,359 +39,5 @@ function delete_client_script_for_doctype(doctype) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTransformExpressionFunction(universal_fields, report_fields, table_doctypes) {
|
// Кодогенерация движка убрана: сгенерированный Client Script теперь несёт только
|
||||||
// Получаем список всех табличных частей динамически
|
// CONFIG, а считает общий рантайм taxes_az.formula. Здесь остался только UI редактора.
|
||||||
const tableNames = table_doctypes ? Object.keys(table_doctypes) : [];
|
|
||||||
const tablePrefixesJson = JSON.stringify(tableNames);
|
|
||||||
|
|
||||||
return `
|
|
||||||
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_PLACEHOLDER_\${reportCalls.length}__\`;
|
|
||||||
|
|
||||||
// Ищем соответствующее поле отчета по имени
|
|
||||||
${Object.keys(report_fields).map(fieldId => {
|
|
||||||
const field = report_fields[fieldId];
|
|
||||||
if (!field || !field.name) return '';
|
|
||||||
|
|
||||||
let filtersJson = "{}";
|
|
||||||
try {
|
|
||||||
const filtersObj = JSON.parse(field.filters_json || "{}");
|
|
||||||
|
|
||||||
if (filtersObj.company && typeof filtersObj.company === 'string' &&
|
|
||||||
!filtersObj.company.startsWith('$')) {
|
|
||||||
filtersObj.company = "$DefaultCompany";
|
|
||||||
}
|
|
||||||
|
|
||||||
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."))) {
|
|
||||||
// Сохраняем оригинальное значение
|
|
||||||
}
|
|
||||||
else if (typeof value === 'string') {
|
|
||||||
filtersObj[key] = "$AyIlSpecial";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
filtersJson = JSON.stringify(filtersObj);
|
|
||||||
} catch (e) {
|
|
||||||
filtersJson = "{}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
if (reportName === "${field.name}") {
|
|
||||||
reportCalls.push({
|
|
||||||
placeholder: placeholder,
|
|
||||||
reportName: "${field.report}",
|
|
||||||
filters: JSON.parse(${JSON.stringify(filtersJson)}),
|
|
||||||
fieldName: "${field.field_name}",
|
|
||||||
aggregation: "${field.aggregation || 'sum'}"
|
|
||||||
});
|
|
||||||
|
|
||||||
transformed = transformed.replace(fullMatch, placeholder);
|
|
||||||
}`;
|
|
||||||
}).join('')}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Обработка table(...) ===
|
|
||||||
const tableRegex = /table\\(([^,)]+),\\s*([^,)]+),\\s*([^)]+)\\)/g;
|
|
||||||
let tableMatch;
|
|
||||||
while ((tableMatch = tableRegex.exec(expression)) !== null) {
|
|
||||||
const fullMatch = tableMatch[0];
|
|
||||||
const tableName = tableMatch[1].trim().replace(/['"]/g, '');
|
|
||||||
const rowNumber = tableMatch[2].trim();
|
|
||||||
const fieldName = tableMatch[3].trim().replace(/['"]/g, '');
|
|
||||||
|
|
||||||
const replacement = \`(function() {
|
|
||||||
try {
|
|
||||||
if (!frm.doc["\${tableName}"] || !frm.doc["\${tableName}"][\${rowNumber} - 1]) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const value = frm.doc["\${tableName}"][\${rowNumber} - 1]["\${fieldName}"];
|
|
||||||
return parseFloat(value) || 0;
|
|
||||||
} catch (e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
})()\`;
|
|
||||||
|
|
||||||
transformed = transformed.replace(fullMatch, replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Обработка universal полей ===
|
|
||||||
${Object.keys(universal_fields).map(fieldId => {
|
|
||||||
const field = universal_fields[fieldId];
|
|
||||||
if (!field || !field.name) return '';
|
|
||||||
return `transformed = transformed.replace(/univ\\(${field.name}\\)/g, "(frm.doc.__universalFields && frm.doc.__universalFields['${fieldId}'] || 0)");`;
|
|
||||||
}).filter(Boolean).join('\n')}
|
|
||||||
|
|
||||||
// === Обработка val() и row() ===
|
|
||||||
// Динамически получаем список всех табличных частей из DocType
|
|
||||||
const tablePrefixes = ${tablePrefixesJson};
|
|
||||||
|
|
||||||
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(/row\\(([^)]+)\\)/g, (m, f) =>
|
|
||||||
\`(parseFloat(row["\${f}"]) || 0)\`
|
|
||||||
);
|
|
||||||
|
|
||||||
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)\`;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
transformed = transformed.replace(/val\\(([^)]+)\\)/g, (m, f) =>
|
|
||||||
\`(parseFloat(frm.doc["\${f}"]) || 0)\`
|
|
||||||
);
|
|
||||||
|
|
||||||
transformed = transformed.replace(/row\\(([^)]+)\\)/g, () =>
|
|
||||||
\`0 /* ERROR: row() вне табличной части */\`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { transformed, needsAsync, reportCalls };
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateTableRowAccessFunction() {
|
|
||||||
return `
|
|
||||||
// Функция для получения индекса строки по её имени (name/cdn)
|
|
||||||
function get_row_index(frm, tableName, rowName) {
|
|
||||||
|
|
||||||
// Проверяем наличие таблицы
|
|
||||||
if (!frm.doc[tableName]) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выводим все строки для отладки
|
|
||||||
frm.doc[tableName].forEach((row, idx) => {
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ищем индекс строки по имени
|
|
||||||
for (let i = 0; i < frm.doc[tableName].length; i++) {
|
|
||||||
if (frm.doc[tableName][i].name === rowName) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для доступа к полям конкретных строк таблицы
|
|
||||||
function table_row(tableName, rowIndex, fieldName) {
|
|
||||||
// Проверяем наличие необходимых аргументов
|
|
||||||
if (!tableName || rowIndex === undefined || !fieldName) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Преобразуем rowIndex в число (на случай, если передана строка)
|
|
||||||
const rowIdx = parseInt(rowIndex) - 1; // Индексация с 1 для пользователя, но с 0 в массиве
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем наличие таблицы и строки
|
|
||||||
if (!frm.doc[tableName] || !frm.doc[tableName][rowIdx]) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем значение поля
|
|
||||||
const value = frm.doc[tableName][rowIdx][fieldName];
|
|
||||||
|
|
||||||
// Проверяем на NaN и возвращаем значение или 0
|
|
||||||
const numValue = parseFloat(value);
|
|
||||||
if (isNaN(numValue)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return numValue;
|
|
||||||
} catch (error) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для обновления значения поля конкретной строки таблицы
|
|
||||||
function set_table_row(frm, tableName, rowIndex, fieldName, value) {
|
|
||||||
// Проверяем наличие необходимых аргументов
|
|
||||||
if (!frm || !tableName || rowIndex === undefined || !fieldName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Преобразуем rowIndex в число (на случай, если передана строка)
|
|
||||||
const rowIdx = parseInt(rowIndex) - 1; // Индексация с 1 для пользователя, но с 0 в массиве
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем наличие таблицы и строки
|
|
||||||
if (!frm.doc[tableName] || !frm.doc[tableName][rowIdx]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем строку таблицы
|
|
||||||
const row = frm.doc[tableName][rowIdx];
|
|
||||||
|
|
||||||
// Получаем сетевое имя cdt и cdn для Frappe
|
|
||||||
const cdt = frm.fields_dict[tableName].grid.doctype;
|
|
||||||
const cdn = row.name;
|
|
||||||
|
|
||||||
// Проверяем, является ли значение NaN, и если да, заменяем его на 0
|
|
||||||
let finalValue = value;
|
|
||||||
if (typeof value === 'number' && isNaN(value)) {
|
|
||||||
finalValue = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем значение поля
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldName, finalValue);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для поиска строки таблицы по значению поля
|
|
||||||
function find_table_row(frm, tableName, fieldName, value) {
|
|
||||||
// Проверяем наличие необходимых аргументов
|
|
||||||
if (!frm || !tableName || !fieldName) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем наличие таблицы
|
|
||||||
if (!frm.doc[tableName]) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ищем строку с указанным значением поля
|
|
||||||
for (let i = 0; i < frm.doc[tableName].length; i++) {
|
|
||||||
if (frm.doc[tableName][i][fieldName] == value) {
|
|
||||||
// Возвращаем индекс строки (начиная с 1 для пользователя)
|
|
||||||
return i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если строка не найдена
|
|
||||||
return -1;
|
|
||||||
} catch (error) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_child_table_script(table_child_scripts, table_doctypes, table_field_triggers, formula_data, functions) {
|
|
||||||
let child_scripts = '';
|
|
||||||
|
|
||||||
Object.keys(table_child_scripts).forEach(tableName => {
|
|
||||||
let childDoctype = table_child_scripts[tableName].doctype;
|
|
||||||
let usesUniversalFields = table_child_scripts[tableName].uses_universal;
|
|
||||||
let table_script = `\n\nfrappe.ui.form.on("${childDoctype}", {\n`;
|
|
||||||
|
|
||||||
// Создаем общую функцию для расчета формул в строке таблицы
|
|
||||||
let calcFunction = `
|
|
||||||
async function calculate_${tableName}_row(frm, cdt, cdn) {
|
|
||||||
let row = locals[cdt][cdn];
|
|
||||||
|
|
||||||
try {
|
|
||||||
${usesUniversalFields ? 'await calculate_universal_fields(frm);' : ''}
|
|
||||||
|
|
||||||
const { transformed, needsAsync, reportCalls } = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}");
|
|
||||||
|
|
||||||
let finalExpression = transformed;
|
|
||||||
|
|
||||||
if (reportCalls && reportCalls.length > 0) {
|
|
||||||
const reportResults = [];
|
|
||||||
|
|
||||||
for (const call of reportCalls) {
|
|
||||||
const result = await getReportFieldValue(
|
|
||||||
frm,
|
|
||||||
call.reportName,
|
|
||||||
call.filters,
|
|
||||||
call.fieldName,
|
|
||||||
call.aggregation
|
|
||||||
);
|
|
||||||
reportResults.push({ placeholder: call.placeholder, value: result });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const result of reportResults) {
|
|
||||||
finalExpression = finalExpression.replace(result.placeholder, result.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = 0;
|
|
||||||
try {
|
|
||||||
result = eval(finalExpression);
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем значение
|
|
||||||
row["${calc.field}"] = isNaN(result) ? 0 : result;
|
|
||||||
frm.refresh_field("${tableName}");
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
table_child_scripts[tableName].calculations.forEach(calc => {
|
|
||||||
calcFunction += `
|
|
||||||
// Расчет для поля ${calc.field}
|
|
||||||
try {
|
|
||||||
const transformed = transform_expression(${JSON.stringify(calc.formula)}, true, "${tableName}").transformed;
|
|
||||||
row["${calc.field}"] = eval(transformed);
|
|
||||||
} catch (e) {
|
|
||||||
row["${calc.field}"] = 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
calcFunction += `
|
|
||||||
frm.refresh_field("${tableName}");
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
functions.push(calcFunction);
|
|
||||||
|
|
||||||
// Добавляем обработчики для каждого триггерного поля
|
|
||||||
Array.from(table_child_scripts[tableName].fields).forEach(field => {
|
|
||||||
if (usesUniversalFields) {
|
|
||||||
table_script += ` "${field}": async function(frm, cdt, cdn) { await calculate_${tableName}_row(frm, cdt, cdn); },\n`;
|
|
||||||
} else {
|
|
||||||
table_script += ` "${field}": function(frm, cdt, cdn) { calculate_${tableName}_row(frm, cdt, cdn); },\n`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
table_script += `});`;
|
|
||||||
child_scripts += table_script;
|
|
||||||
});
|
|
||||||
|
|
||||||
return child_scripts;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -754,269 +754,5 @@ function deleteReportField(frm, fieldId) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateReportFieldsFunction(report_fields) {
|
// Кодогенерация движка убрана: сгенерированный Client Script теперь несёт только
|
||||||
if (!report_fields || Object.keys(report_fields).length === 0) {
|
// CONFIG, а считает общий рантайм taxes_az.formula. Здесь остался только UI редактора.
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
// Функция для обработки азербайджанских дат
|
|
||||||
function processAzerbayjanDateField(docValues) {
|
|
||||||
// Словарь для сопоставления названий месяцев с их номерами
|
|
||||||
const monthNames = {
|
|
||||||
"Yanvar": 1,
|
|
||||||
"Fevral": 2,
|
|
||||||
"Mart": 3,
|
|
||||||
"Aprel": 4,
|
|
||||||
"May": 5,
|
|
||||||
"İyun": 6,
|
|
||||||
"İyul": 7,
|
|
||||||
"Avqust": 8,
|
|
||||||
"Sentyabr": 9,
|
|
||||||
"Oktyabr": 10,
|
|
||||||
"Noyabr": 11,
|
|
||||||
"Dekabr": 12
|
|
||||||
};
|
|
||||||
|
|
||||||
// Проверяем наличие квартала (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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, есть ли у нас значения ay и il
|
|
||||||
if (docValues && docValues.ay && monthNames[docValues.ay]) {
|
|
||||||
// Получаем номер месяца
|
|
||||||
const monthNum = monthNames[docValues.ay];
|
|
||||||
|
|
||||||
// Получаем год из поля il или используем текущий год
|
|
||||||
let year = docValues.il || new Date().getFullYear();
|
|
||||||
|
|
||||||
// Определяем первый день месяца (1-е число)
|
|
||||||
const firstDay = new Date(year, monthNum - 1, 1);
|
|
||||||
|
|
||||||
// Последний день месяца (получаем последнее число месяца)
|
|
||||||
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');
|
|
||||||
|
|
||||||
// Возвращаем объект с диапазоном дат
|
|
||||||
return {
|
|
||||||
start: firstDayStr,
|
|
||||||
end: lastDayStr
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если это не специальное поле или значение не распознано, возвращаем null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getReportFieldValue(frm, reportName, filters, fieldName, aggregation) {
|
|
||||||
try {
|
|
||||||
// Обрабатываем фильтры с возможными ссылками на поля документа
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка специальных переменных
|
|
||||||
if (typeof value === 'string' && value.startsWith('$')) {
|
|
||||||
switch (value) {
|
|
||||||
case '$DefaultCompany':
|
|
||||||
// Получаем компанию по умолчанию из настроек или документа
|
|
||||||
processedFilters[key] = frm.doc.company || frappe.defaults.get_user_default("Company");
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case '$AyIlSpecial':
|
|
||||||
// Обрабатываем специальные даты для азербайджанских месяцев/кварталов
|
|
||||||
const dateRange = processAzerbayjanDateField(frm.doc);
|
|
||||||
if (dateRange) {
|
|
||||||
// Для полей from_date и to_date используем соответствующие значения
|
|
||||||
if (key === 'from_date') {
|
|
||||||
processedFilters[key] = dateRange.start;
|
|
||||||
} else if (key === 'to_date') {
|
|
||||||
processedFilters[key] = dateRange.end;
|
|
||||||
} else {
|
|
||||||
// Для других полей используем начальную дату
|
|
||||||
processedFilters[key] = dateRange.start;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Если не удалось получить диапазон дат, используем текущую дату
|
|
||||||
processedFilters[key] = frappe.datetime.get_today();
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Для других $-переменных оставляем как есть
|
|
||||||
processedFilters[key] = value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обычные значения
|
|
||||||
processedFilters[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем кеш
|
|
||||||
if (!window.reportCache) window.reportCache = {};
|
|
||||||
|
|
||||||
const cacheKey = reportName + "|" + JSON.stringify(processedFilters) + "|" + fieldName + "|" + aggregation;
|
|
||||||
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 !== undefined) {
|
|
||||||
if (result.message.error) {
|
|
||||||
console.error('Report error:', result.message.error);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = parseFloat(result.message);
|
|
||||||
if (isNaN(value)) value = 0;
|
|
||||||
|
|
||||||
// Округляем до 2 знаков после запятой
|
|
||||||
value = Math.round(value * 100) / 100;
|
|
||||||
|
|
||||||
// Сохраняем в кеш
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для применения агрегации к данным отчета
|
|
||||||
function applyAggregation(data, fieldName, aggregationType) {
|
|
||||||
if (!data || data.length === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = data.map(row => {
|
|
||||||
let value = null;
|
|
||||||
if (typeof row === 'object' && row[fieldName] !== undefined) {
|
|
||||||
value = row[fieldName];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Преобразуем в число
|
|
||||||
const numValue = parseFloat(value);
|
|
||||||
return isNaN(numValue) ? 0 : numValue;
|
|
||||||
}).filter(v => v !== 0); // Убираем нулевые значения
|
|
||||||
|
|
||||||
if (values.length === 0) return 0;
|
|
||||||
|
|
||||||
switch(aggregationType) {
|
|
||||||
case 'sum':
|
|
||||||
return values.reduce((sum, val) => sum + val, 0);
|
|
||||||
case 'avg':
|
|
||||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
|
||||||
case 'min':
|
|
||||||
return Math.min(...values);
|
|
||||||
case 'max':
|
|
||||||
return Math.max(...values);
|
|
||||||
case 'first':
|
|
||||||
return values[0] || 0;
|
|
||||||
case 'last':
|
|
||||||
return values[values.length - 1] || 0;
|
|
||||||
default:
|
|
||||||
return values.reduce((sum, val) => sum + val, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Экспорт функций для использования в других модулях
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = {
|
|
||||||
initReportFields,
|
|
||||||
generateReportFieldsFunction,
|
|
||||||
openReportFieldModal,
|
|
||||||
loadReportFields,
|
|
||||||
loadReportFilters
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1189,294 +1189,5 @@ function openTableRowFieldSelector(frm, targetContainer, isForTrigger = false) {
|
||||||
$modal.modal('show');
|
$modal.modal('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTableRowFormulasHandlers(table_row_formulas, functions, target_doctype, universal_fields) {
|
// Кодогенерация движка убрана: сгенерированный Client Script теперь несёт только
|
||||||
// Создаем функцию для инициализации формул строк табличных частей
|
// CONFIG, а считает общий рантайм taxes_az.formula. Здесь остался только UI редактора.
|
||||||
let initFunction = `
|
|
||||||
// Функция для инициализации формул строк табличных частей
|
|
||||||
function initialize_table_row_formulas(frm) {
|
|
||||||
try {
|
|
||||||
// Получаем информацию о формулах для строк табличных частей
|
|
||||||
const formulas = JSON.parse(frm.doc.table_row_formulas_storage || "{}");
|
|
||||||
|
|
||||||
// ВАЖНО: Преобразуем объект в массив и сортируем его по порядку выполнения
|
|
||||||
const sortedFormulas = Object.entries(formulas).map(([id, formula]) => {
|
|
||||||
return { id, ...formula };
|
|
||||||
}).sort((a, b) => {
|
|
||||||
// Сортировка по значению поля order (если порядок не задан, используем 10 по умолчанию)
|
|
||||||
const orderA = a.order !== undefined ? parseInt(a.order) : 10;
|
|
||||||
const orderB = b.order !== undefined ? parseInt(b.order) : 10;
|
|
||||||
return orderA - orderB;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Группируем формулы по табличным частям и строкам
|
|
||||||
const formulasByTableAndRow = {};
|
|
||||||
|
|
||||||
sortedFormulas.forEach(formula => {
|
|
||||||
const tableName = formula.table_name;
|
|
||||||
const rowIndex = parseInt(formula.row_index);
|
|
||||||
|
|
||||||
if (!formulasByTableAndRow[tableName]) {
|
|
||||||
formulasByTableAndRow[tableName] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formulasByTableAndRow[tableName][rowIndex]) {
|
|
||||||
formulasByTableAndRow[tableName][rowIndex] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
formulasByTableAndRow[tableName][rowIndex].push(formula);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Устанавливаем обработчики для каждой табличной части
|
|
||||||
Object.keys(formulasByTableAndRow).forEach(tableName => {
|
|
||||||
if (!frm.doc[tableName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Для каждой строки таблицы
|
|
||||||
frm.doc[tableName].forEach((row, rowIdx) => {
|
|
||||||
// Получаем отсортированные формулы для текущей строки (строки нумеруются с 1, а индексы с 0)
|
|
||||||
const rowFormulas = formulasByTableAndRow[tableName][rowIdx + 1] || [];
|
|
||||||
|
|
||||||
// Выполняем формулы в порядке сортировки (по полю order)
|
|
||||||
for (const formula of rowFormulas) {
|
|
||||||
try {
|
|
||||||
// Определяем необходимость асинхронного вызова
|
|
||||||
const usesUniversal = formula.formula && formula.formula.includes("univ(");
|
|
||||||
const usesReports = formula.formula && formula.formula.includes("report(");
|
|
||||||
|
|
||||||
// Вызываем расчет формулы согласно установленному порядку
|
|
||||||
if (usesUniversal || usesReports) {
|
|
||||||
// Для асинхронных вызовов используем await в асинхронной IIFE
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
await calculate_table_row_field(frm, tableName, rowIdx, formula.field_name, formula.formula, "refresh");
|
|
||||||
} catch (asyncError) {
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
} else {
|
|
||||||
// Для синхронных вызовов выполняем напрямую
|
|
||||||
calculate_table_row_field(frm, tableName, rowIdx, formula.field_name, formula.formula, "refresh");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
functions.push(initFunction);
|
|
||||||
|
|
||||||
// Создаем функцию для расчета значения поля строки таблицы
|
|
||||||
let calculateFunction = `
|
|
||||||
// Функция для расчета значения поля в строке табличной части
|
|
||||||
async function calculate_table_row_field(frm, tableName, rowIndex, fieldName, formula, triggerField) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
if (!frm.doc[tableName] || !frm.doc[tableName][rowIndex]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = frm.doc[tableName][rowIndex];
|
|
||||||
const cdt = frm.fields_dict[tableName].grid.doctype;
|
|
||||||
const cdn = row.name;
|
|
||||||
|
|
||||||
// === ЕДИНАЯ ТОЧКА ОБРАБОТКИ ВСЕХ ФУНКЦИЙ ===
|
|
||||||
let finalFormula, reportCalls;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Используем transform_expression как единственную точку преобразования
|
|
||||||
const transformResult = transform_expression(formula, true, tableName);
|
|
||||||
|
|
||||||
if (transformResult) {
|
|
||||||
finalFormula = transformResult.transformed;
|
|
||||||
reportCalls = transformResult.reportCalls || [];
|
|
||||||
} else {
|
|
||||||
finalFormula = formula;
|
|
||||||
reportCalls = [];
|
|
||||||
}
|
|
||||||
} catch (transformError) {
|
|
||||||
console.error('Ошибка transform_expression:', transformError);
|
|
||||||
finalFormula = formula;
|
|
||||||
reportCalls = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// === ОБРАБОТКА АСИНХРОННЫХ ВЫЗОВОВ ОТЧЕТОВ ===
|
|
||||||
if (reportCalls && reportCalls.length > 0) {
|
|
||||||
try {
|
|
||||||
// Выполняем все асинхронные запросы к отчетам параллельно
|
|
||||||
const reportResults = await Promise.all(
|
|
||||||
reportCalls.map(call =>
|
|
||||||
getReportFieldValue(
|
|
||||||
frm,
|
|
||||||
call.reportName,
|
|
||||||
call.filters,
|
|
||||||
call.fieldName,
|
|
||||||
call.aggregation
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Заменяем плейсхолдеры на реальные значения
|
|
||||||
for (let i = 0; i < reportCalls.length; i++) {
|
|
||||||
const reportValue = reportResults[i] !== undefined ? Number(reportResults[i]) : 0;
|
|
||||||
finalFormula = finalFormula.replace(
|
|
||||||
reportCalls[i].placeholder,
|
|
||||||
reportValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (reportError) {
|
|
||||||
console.error('Ошибка получения данных отчетов:', reportError);
|
|
||||||
// В случае ошибки заменяем все плейсхолдеры на 0
|
|
||||||
for (const call of reportCalls) {
|
|
||||||
finalFormula = finalFormula.replace(call.placeholder, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === ВЫЧИСЛЕНИЕ ФИНАЛЬНОЙ ФОРМУЛЫ ===
|
|
||||||
let result = 0;
|
|
||||||
try {
|
|
||||||
// Создаем функцию с доступом к контексту row и frm
|
|
||||||
const evalFunc = new Function('row', 'frm', \`return \${finalFormula};\`);
|
|
||||||
result = evalFunc(row, frm);
|
|
||||||
} catch (evalError) {
|
|
||||||
console.error('Ошибка вычисления формулы:', evalError, 'Formula:', finalFormula);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(result)) {
|
|
||||||
result = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Устанавливаем вычисленное значение
|
|
||||||
frappe.model.set_value(cdt, cdn, fieldName, result);
|
|
||||||
|
|
||||||
// Обновляем таблицу
|
|
||||||
frm.refresh_field(tableName);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка в calculate_table_row_field:', err);
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
functions.push(calculateFunction);
|
|
||||||
|
|
||||||
// Функция для получения индекса строки таблицы
|
|
||||||
let getRowIndexFunction = `
|
|
||||||
// Функция для получения индекса строки по её имени
|
|
||||||
function get_row_index(frm, tableName, rowName) {
|
|
||||||
|
|
||||||
// Проверяем наличие таблицы
|
|
||||||
if (!frm.doc[tableName]) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ищем индекс строки по имени
|
|
||||||
for (let i = 0; i < frm.doc[tableName].length; i++) {
|
|
||||||
if (frm.doc[tableName][i].name === rowName) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
functions.push(getRowIndexFunction);
|
|
||||||
|
|
||||||
return functions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function processTableRowFormulas(table_row_formulas, table_doctypes) {
|
|
||||||
// Хранилище для объединенных обработчиков
|
|
||||||
const triggerHandlers = {};
|
|
||||||
|
|
||||||
// Проверяем, что объект table_row_formulas не пустой
|
|
||||||
if (!table_row_formulas || Object.keys(table_row_formulas).length === 0) {
|
|
||||||
return triggerHandlers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем отсортированные формулы по порядку выполнения
|
|
||||||
const sortedFormulas = Object.entries(table_row_formulas).map(([id, formula]) => {
|
|
||||||
return { id, ...formula };
|
|
||||||
}).sort((a, b) => {
|
|
||||||
// Сортировка по значению поля order (если порядок не задан, используем 10 по умолчанию)
|
|
||||||
const orderA = a.order !== undefined ? parseInt(a.order) : 10;
|
|
||||||
const orderB = b.order !== undefined ? parseInt(b.order) : 10;
|
|
||||||
return orderA - orderB;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Группируем формулы по триггерам
|
|
||||||
sortedFormulas.forEach(formula => {
|
|
||||||
const tableName = formula.table_name;
|
|
||||||
const rowIndex = parseInt(formula.row_index);
|
|
||||||
const usesUniversal = formula.formula && formula.formula.includes("univ(");
|
|
||||||
const usesReports = formula.formula && formula.formula.includes("report(");
|
|
||||||
|
|
||||||
// Если есть триггеры, добавляем их
|
|
||||||
if (formula.triggers) {
|
|
||||||
const triggers = formula.triggers.split(',').map(t => t.trim());
|
|
||||||
|
|
||||||
triggers.forEach(trigger => {
|
|
||||||
// Пропускаем триггер "При открытии документа", он обрабатывается отдельно
|
|
||||||
if (!trigger || trigger === "При открытии документа" ||
|
|
||||||
trigger === "when_document_opens" || trigger === "document_opens") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализируем обработчик триггера, если его еще нет
|
|
||||||
if (!triggerHandlers[trigger]) {
|
|
||||||
triggerHandlers[trigger] = {
|
|
||||||
isAsync: usesUniversal || usesReports,
|
|
||||||
handlers: []
|
|
||||||
};
|
|
||||||
} else if (usesUniversal || usesReports) {
|
|
||||||
// Если текущая формула требует async, помечаем весь обработчик как async
|
|
||||||
triggerHandlers[trigger].isAsync = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Код для вычисления формулы, с учетом порядка выполнения
|
|
||||||
const handlerCode = `
|
|
||||||
// Формула строки ${rowIndex} в таблице ${tableName} (порядок: ${formula.order || 10})
|
|
||||||
if (frm.doc.${tableName} && frm.doc.${tableName}.length >= ${rowIndex}) {
|
|
||||||
try {
|
|
||||||
// Индекс строки на 1 меньше номера строки
|
|
||||||
const rowIndex = ${rowIndex - 1};
|
|
||||||
const formula = "${formula.formula}";
|
|
||||||
|
|
||||||
${usesUniversal || usesReports ? 'await ' : ''}calculate_table_row_field(frm, "${tableName}", rowIndex, "${formula.field_name}", formula, "${trigger}");
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
triggerHandlers[trigger].handlers.push(handlerCode);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем обработчики для событий add_row и remove_row для каждой таблицы
|
|
||||||
const tables = new Set();
|
|
||||||
sortedFormulas.forEach(formula => {
|
|
||||||
tables.add(formula.table_name);
|
|
||||||
});
|
|
||||||
|
|
||||||
tables.forEach(tableName => {
|
|
||||||
// Add handler
|
|
||||||
triggerHandlers[`${tableName}_add`] = {
|
|
||||||
isAsync: false,
|
|
||||||
handlers: [
|
|
||||||
`// Обновляем индексы строк после добавления новой строки`,
|
|
||||||
`setTimeout(() => initialize_table_row_formulas(frm), 100);`
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove handler
|
|
||||||
triggerHandlers[`${tableName}_remove`] = {
|
|
||||||
isAsync: false,
|
|
||||||
handlers: [
|
|
||||||
`// Обновляем индексы строк после удаления строки`,
|
|
||||||
`setTimeout(() => initialize_table_row_formulas(frm), 100);`
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return triggerHandlers;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1085,293 +1085,5 @@ function renderUniversalFields(frm) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateUniversalFieldsFunction(universal_fields) {
|
// Кодогенерация движка убрана: сгенерированный Client Script теперь несёт только
|
||||||
let universalFieldsCode =
|
// CONFIG, а считает общий рантайм taxes_az.formula. Здесь остался только UI редактора.
|
||||||
`
|
|
||||||
// Функция для расчета универсальных полей
|
|
||||||
async function calculate_universal_fields(frm) {
|
|
||||||
${Object.keys(universal_fields).map(fieldId => {
|
|
||||||
const field = universal_fields[fieldId];
|
|
||||||
if (!field || !field.name || !field.doctype || !field.fieldname) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка фильтров для ссылок на поля документа
|
|
||||||
const processedFilters = (field.filters || []).map(filter => {
|
|
||||||
let newFilter = {...filter};
|
|
||||||
|
|
||||||
// Проверяем, является ли значение ссылкой на поле документа
|
|
||||||
if (typeof newFilter.value === 'string' && newFilter.value.startsWith('doc.')) {
|
|
||||||
const docField = newFilter.value.substring(4); // Убираем 'doc.'
|
|
||||||
newFilter.valueType = 'field';
|
|
||||||
newFilter.docField = docField;
|
|
||||||
|
|
||||||
// Проверяем, является ли это специальным полем 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';
|
|
||||||
}
|
|
||||||
return newFilter;
|
|
||||||
});
|
|
||||||
|
|
||||||
return `
|
|
||||||
// Расчет универсального поля: ${field.name}
|
|
||||||
try {
|
|
||||||
let univ_${fieldId} = 0;
|
|
||||||
// Создаем массив фильтров
|
|
||||||
let filters_${fieldId} = [];
|
|
||||||
|
|
||||||
// Обрабатываем фильтры с учетом возможных ссылок на поля документа
|
|
||||||
let skipCalculation_${fieldId} = false;
|
|
||||||
|
|
||||||
${processedFilters.map(filter => {
|
|
||||||
if (filter.valueType === 'field') {
|
|
||||||
if (filter.__is_special_ay_field) {
|
|
||||||
return `
|
|
||||||
// Фильтр со ссылкой на специальное поле месяца (ay) или года (il)
|
|
||||||
if (frm.doc["ay"] !== undefined && frm.doc["il"] !== undefined) {
|
|
||||||
// Получаем значения месяца и года
|
|
||||||
const docValues = {
|
|
||||||
ay: frm.doc["ay"],
|
|
||||||
il: frm.doc["il"]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Проверяем, что поле ay имеет допустимое значение
|
|
||||||
if (!docValues.ay || typeof docValues.ay !== 'string') {
|
|
||||||
skipCalculation_${fieldId} = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, что поле il имеет допустимое значение
|
|
||||||
if (!docValues.il || isNaN(parseInt(docValues.il))) {
|
|
||||||
skipCalculation_${fieldId} = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обрабатываем специальные поля 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] === ">="))
|
|
||||||
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 {
|
|
||||||
return `
|
|
||||||
// Фильтр со ссылкой на поле документа: ${filter.docField}
|
|
||||||
if (frm.doc["${filter.docField}"] !== undefined) {
|
|
||||||
filters_${fieldId}.push(["${filter.field}", "${filter.operator}", frm.doc["${filter.docField}"]]);
|
|
||||||
} else {
|
|
||||||
skipCalculation_${fieldId} = true;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Обычные статические фильтры
|
|
||||||
return `
|
|
||||||
// Статический фильтр
|
|
||||||
filters_${fieldId}.push(["${filter.field}", "${filter.operator}", "${filter.value}"]);
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}).join('\n ')}
|
|
||||||
|
|
||||||
// Проверяем, нужно ли пропустить расчет
|
|
||||||
if (skipCalculation_${fieldId}) {
|
|
||||||
if (!frm.doc.__universalFields) frm.doc.__universalFields = {};
|
|
||||||
frm.doc.__universalFields["${fieldId}"] = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result_${fieldId} = await frappe.db.get_list("${field.doctype}", {
|
|
||||||
fields: ["${field.fieldname}"],
|
|
||||||
filters: filters_${fieldId},
|
|
||||||
limit: 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result_${fieldId} && result_${fieldId}.length > 0) {
|
|
||||||
result_${fieldId}.forEach(r => {
|
|
||||||
univ_${fieldId} += (parseFloat(r.${field.fieldname}) || 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем результат в временное поле документа
|
|
||||||
if (!frm.doc.__universalFields) frm.doc.__universalFields = {};
|
|
||||||
frm.doc.__universalFields["${fieldId}"] = univ_${fieldId};
|
|
||||||
} catch (error) {
|
|
||||||
if (!frm.doc.__universalFields) frm.doc.__universalFields = {};
|
|
||||||
frm.doc.__universalFields["${fieldId}"] = 0;
|
|
||||||
}`;
|
|
||||||
}).filter(code => code !== '').join('\n')}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Добавляем функцию для обработки специальных полей с кварталом
|
|
||||||
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) {
|
|
||||||
// Словарь для сопоставления названий месяцев с их номерами
|
|
||||||
const monthNames = {
|
|
||||||
"Yanvar": 1,
|
|
||||||
"Fevral": 2,
|
|
||||||
"Mart": 3,
|
|
||||||
"Aprel": 4,
|
|
||||||
"May": 5,
|
|
||||||
"İyun": 6,
|
|
||||||
"İyul": 7,
|
|
||||||
"Avqust": 8,
|
|
||||||
"Sentyabr": 9,
|
|
||||||
"Oktyabr": 10,
|
|
||||||
"Noyabr": 11,
|
|
||||||
"Dekabr": 12
|
|
||||||
};
|
|
||||||
|
|
||||||
// Проверяем наличие квартала (rüb) и года (il)
|
|
||||||
if (docValues && docValues.rüb && docValues.il) {
|
|
||||||
// Вызываем специальную функцию для обработки квартала
|
|
||||||
return processQuarterDateField(docValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, есть ли у нас значения ay и il
|
|
||||||
if (docValues && docValues.ay && monthNames[docValues.ay]) {
|
|
||||||
// Получаем номер месяца
|
|
||||||
const monthNum = monthNames[docValues.ay];
|
|
||||||
|
|
||||||
// Получаем год из поля il или используем текущий год
|
|
||||||
let year = docValues.il || new Date().getFullYear();
|
|
||||||
|
|
||||||
// Определяем первый день месяца (1-е число)
|
|
||||||
const firstDay = new Date(year, monthNum - 1, 1);
|
|
||||||
|
|
||||||
// Последний день месяца (получаем последнее число месяца)
|
|
||||||
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')}\`;
|
|
||||||
|
|
||||||
// Возвращаем объект с диапазоном дат
|
|
||||||
return {
|
|
||||||
start: firstDayStr,
|
|
||||||
end: lastDayStr
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если это не специальное поле или значение не распознано, возвращаем null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
return universalFieldsCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,24 +38,6 @@ function getTableDoctypes(target_doctype, table_doctypes) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectChildTables(formula_data, child_tables) {
|
|
||||||
Object.keys(formula_data).forEach(fieldname => {
|
|
||||||
let field_info = formula_data[fieldname];
|
|
||||||
|
|
||||||
if (field_info && field_info.is_table_field) {
|
|
||||||
// Извлекаем имя табличной части из fieldname (формат: tablename_fieldname)
|
|
||||||
let parts = fieldname.split('_');
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
let table_name = parts[0];
|
|
||||||
if (!child_tables[table_name]) {
|
|
||||||
child_tables[table_name] = [];
|
|
||||||
}
|
|
||||||
child_tables[table_name].push(fieldname);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function processAzerbayjanDateField(docValues) {
|
function processAzerbayjanDateField(docValues) {
|
||||||
// Словарь для сопоставления названий месяцев с их номерами
|
// Словарь для сопоставления названий месяцев с их номерами
|
||||||
const monthNames = {
|
const monthNames = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue