work #1
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -5,6 +5,7 @@ app_description = "Advanced formula editing capabilities for Frappe/ERPNext"
|
|||
app_email = "your.email@example.com"
|
||||
app_license = "MIT"
|
||||
|
||||
|
||||
# Includes in <head>
|
||||
# ------------------
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ app_license = "MIT"
|
|||
|
||||
# include js in doctype views
|
||||
doctype_js = {
|
||||
"DocType": "public/js/doctype_formula_editor.js",
|
||||
"Formula Editor": "formula_editor/doctype/formula_editor/formula_editor_main.js",
|
||||
}
|
||||
|
||||
# include js in doctype list views
|
||||
|
|
|
|||
|
|
@ -0,0 +1,458 @@
|
|||
function initDragAndDrop($modal, $resultContainer, isTableField, frm) {
|
||||
// Очищаем предыдущие обработчики
|
||||
$modal.find('.field-btn, .operator-btn, .universal-field-btn, .report-field-btn').off('dragstart');
|
||||
$resultContainer.off('dragover dragleave drop');
|
||||
|
||||
// Настраиваем перетаскивание существующих токенов
|
||||
setupTokenDragging($resultContainer);
|
||||
|
||||
// Настраиваем перетаскивание полей из панели
|
||||
$modal.find('.field-btn').off('dragstart').on('dragstart', function(e) {
|
||||
const fieldname = $(this).data('fieldname');
|
||||
const label = $(this).data('label');
|
||||
const group = $(this).data('group');
|
||||
|
||||
// Проверяем принадлежность к группе
|
||||
const isTableFieldGroup = group !== "Основные поля";
|
||||
const fieldData = {
|
||||
type: 'field',
|
||||
fieldname: fieldname,
|
||||
label: label,
|
||||
group: group,
|
||||
isTableField: isTableFieldGroup
|
||||
};
|
||||
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(fieldData));
|
||||
});
|
||||
|
||||
// Настраиваем перетаскивание универсальных полей
|
||||
$modal.find('.universal-field-btn').off('dragstart').on('dragstart', function(e) {
|
||||
const fieldId = $(this).data('field-id');
|
||||
const fieldname = $(this).data('fieldname');
|
||||
const label = $(this).data('label');
|
||||
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'universal-field',
|
||||
fieldId: fieldId,
|
||||
fieldname: fieldname,
|
||||
label: label
|
||||
}));
|
||||
});
|
||||
|
||||
// Настраиваем перетаскивание полей отчетов
|
||||
$modal.find('.report-field-btn').off('dragstart').on('dragstart', function(e) {
|
||||
const fieldId = $(this).data('field-id');
|
||||
const fieldname = $(this).data('fieldname');
|
||||
const label = $(this).data('label');
|
||||
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'report-field',
|
||||
fieldId: fieldId,
|
||||
fieldname: fieldname,
|
||||
label: label
|
||||
}));
|
||||
});
|
||||
|
||||
// Настраиваем перетаскивание операторов
|
||||
$modal.find('.operator-btn').off('dragstart').on('dragstart', function(e) {
|
||||
const operator = $(this).data('operator');
|
||||
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'operator',
|
||||
value: operator
|
||||
}));
|
||||
});
|
||||
|
||||
// Обработчики для целевого контейнера
|
||||
$resultContainer.off('dragover').on('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$(this).addClass('drag-hover');
|
||||
|
||||
updateDropMarker(e, $(this));
|
||||
});
|
||||
|
||||
$resultContainer.off('dragleave').on('dragleave', function(e) {
|
||||
$(this).removeClass('drag-hover');
|
||||
$(this).find('.drop-marker').remove();
|
||||
});
|
||||
|
||||
$resultContainer.off('drop').on('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$(this).removeClass('drag-hover');
|
||||
$(this).find('.drop-marker').remove();
|
||||
|
||||
handleDrop(e, $(this), isTableField, frm);
|
||||
});
|
||||
}
|
||||
|
||||
function initTriggerDragAndDrop($modal, $triggerContainer) {
|
||||
// Очищаем предыдущие обработчики
|
||||
$modal.find('.field-btn, .universal-field-btn, .trigger-field-btn').off('dragstart');
|
||||
$triggerContainer.off('dragover dragleave drop');
|
||||
|
||||
// Настраиваем перетаскивание существующих токенов в триггер-контейнере
|
||||
$triggerContainer.find('.trigger-token').each(function() {
|
||||
$(this).attr('draggable', 'true');
|
||||
|
||||
$(this).off('dragstart').on('dragstart', function(e) {
|
||||
// Сохраняем ссылку на текущий элемент
|
||||
window.draggedTriggerElement = this;
|
||||
|
||||
// Добавляем данные для переноса
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'trigger-token',
|
||||
value: $(this).data('value'),
|
||||
html: $(this)[0].outerHTML,
|
||||
tokenClass: $(this).attr('class')
|
||||
}));
|
||||
|
||||
// Добавляем класс перетаскивания
|
||||
$(this).addClass('dragging');
|
||||
});
|
||||
|
||||
// При завершении перетаскивания
|
||||
$(this).off('dragend').on('dragend', function() {
|
||||
$(this).removeClass('dragging');
|
||||
window.draggedTriggerElement = null;
|
||||
});
|
||||
});
|
||||
|
||||
// Настраиваем перетаскивание полей из панели в триггер-контейнер
|
||||
$modal.find('.field-btn, .universal-field-btn, .trigger-field-btn').off('dragstart').on('dragstart', function(e) {
|
||||
const fieldname = $(this).data('fieldname');
|
||||
const label = $(this).data('label');
|
||||
let fieldType = 'field';
|
||||
|
||||
if ($(this).hasClass('universal-field-btn')) {
|
||||
fieldType = 'universal';
|
||||
} else if ($(this).hasClass('highlight-event')) {
|
||||
fieldType = 'event';
|
||||
} else if ($(this).hasClass('table-field-btn')) {
|
||||
fieldType = 'table';
|
||||
}
|
||||
|
||||
const fieldData = {
|
||||
type: 'trigger-field',
|
||||
fieldname: fieldname,
|
||||
label: label,
|
||||
fieldType: fieldType
|
||||
};
|
||||
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(fieldData));
|
||||
});
|
||||
|
||||
// Обработка dragover для триггер-контейнера
|
||||
$triggerContainer.off('dragover').on('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$(this).addClass('drag-hover');
|
||||
|
||||
// Обновляем маркер сброса
|
||||
updateDropMarker(e, $(this));
|
||||
});
|
||||
|
||||
// Обработка dragleave для триггер-контейнера
|
||||
$triggerContainer.off('dragleave').on('dragleave', function(e) {
|
||||
e.preventDefault();
|
||||
$(this).removeClass('drag-hover');
|
||||
$(this).find('.drop-marker').remove();
|
||||
});
|
||||
|
||||
// Обработка drop для триггер-контейнера
|
||||
$triggerContainer.off('drop').on('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$(this).removeClass('drag-hover');
|
||||
$(this).find('.drop-marker').remove();
|
||||
|
||||
// Обрабатываем сброшенные данные
|
||||
handleTriggerDrop(e, $(this));
|
||||
});
|
||||
}
|
||||
|
||||
function updateDropMarker(e, $container) {
|
||||
const x = e.originalEvent.clientX;
|
||||
const y = e.originalEvent.clientY;
|
||||
|
||||
let position = { element: null, position: 'after' };
|
||||
|
||||
// Проходим по всем дочерним элементам контейнера
|
||||
$container.contents().each(function() {
|
||||
if (this.nodeType === Node.TEXT_NODE) return;
|
||||
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
// Если курсор находится над элементом
|
||||
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
||||
position.element = this;
|
||||
|
||||
// Определяем, до или после элемента
|
||||
if (x < rect.left + rect.width / 2) {
|
||||
position.position = 'before';
|
||||
} else {
|
||||
position.position = 'after';
|
||||
}
|
||||
|
||||
return false; // Прерываем перебор
|
||||
}
|
||||
});
|
||||
|
||||
$container.find('.drop-marker').remove();
|
||||
|
||||
if (position.element) {
|
||||
const $marker = $('<span class="drop-marker" style="display:inline-block;width:2px;height:18px;background-color:#4285f4;vertical-align:middle;"></span>');
|
||||
|
||||
if (position.position === 'before') {
|
||||
$(position.element).before($marker);
|
||||
} else {
|
||||
$(position.element).after($marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(e, $container, isTableField, frm) {
|
||||
try {
|
||||
// Получаем данные из события перетаскивания
|
||||
const dataTransfer = e.originalEvent.dataTransfer.getData('text/plain');
|
||||
|
||||
// Проверяем, что данные не пустые
|
||||
if (!dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(dataTransfer);
|
||||
|
||||
const x = e.originalEvent.clientX;
|
||||
const y = e.originalEvent.clientY;
|
||||
|
||||
let position = findDropPosition(x, y, $container);
|
||||
|
||||
// Обработка перетаскивания существующих токенов
|
||||
if (data.type === 'token' && window.draggedElement) {
|
||||
handleExistingTokenDrop(window.draggedElement, position, $container);
|
||||
return;
|
||||
}
|
||||
|
||||
// Создание нового элемента
|
||||
let $newElement;
|
||||
|
||||
if (data.type === 'universal-field') {
|
||||
// Получаем универсальные поля
|
||||
const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}");
|
||||
const field = universalFields[data.fieldId];
|
||||
|
||||
if (field) {
|
||||
const formulaText = `univ(${field.name})`;
|
||||
$newElement = $(`<span class="formula-token field universal-field" data-value="${formulaText}" draggable="true" contenteditable="false">${formulaText}<span class="token-remove">×</span></span>`);
|
||||
}
|
||||
} else if (data.type === 'report-field') {
|
||||
// Обработка поля отчета
|
||||
const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
|
||||
const field = reportFields[data.fieldId];
|
||||
|
||||
if (field) {
|
||||
const formulaText = `report(${field.name})`;
|
||||
$newElement = $(`<span class="formula-token field report-field" data-value="${formulaText}" draggable="true" contenteditable="false">${formulaText}<span class="token-remove">×</span></span>`);
|
||||
}
|
||||
} else if (data.type === 'field') {
|
||||
$newElement = createFieldToken(data, isTableField);
|
||||
} else if (data.type === 'operator') {
|
||||
$newElement = createOperatorToken(data.value);
|
||||
}
|
||||
|
||||
// Вставляем новый элемент
|
||||
if ($newElement) {
|
||||
insertElementAtPosition($newElement, position, $container);
|
||||
|
||||
// Инициализируем события для нового элемента
|
||||
initializeTokenEvents($newElement);
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleTriggerDrop(e, $container) {
|
||||
try {
|
||||
const data = JSON.parse(e.originalEvent.dataTransfer.getData('text/plain'));
|
||||
|
||||
// Получаем позицию сброса
|
||||
const x = e.originalEvent.clientX;
|
||||
const y = e.originalEvent.clientY;
|
||||
let position = findDropPosition(x, y, $container);
|
||||
|
||||
// Обработка перетаскивания существующих токенов
|
||||
if (data.type === 'trigger-token' && window.draggedTriggerElement) {
|
||||
const $dragged = $(window.draggedTriggerElement);
|
||||
|
||||
if (position.element) {
|
||||
if (position.position === 'before') {
|
||||
$(position.element).before($dragged);
|
||||
$(position.element).before(' ');
|
||||
} else {
|
||||
$(position.element).after($dragged);
|
||||
$(position.element).after(' ');
|
||||
}
|
||||
} else {
|
||||
$container.append($dragged);
|
||||
$container.append(' ');
|
||||
}
|
||||
|
||||
window.draggedTriggerElement = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка перетаскивания полей из панели
|
||||
if (data.type === 'field' || data.type === 'trigger-field') {
|
||||
// Проверяем, не добавлено ли уже это поле
|
||||
const fieldname = data.fieldname;
|
||||
const existingTokens = $container.find(`.trigger-token[data-value="${fieldname}"]`);
|
||||
if (existingTokens.length > 0) {
|
||||
return; // Если поле уже добавлено, не добавляем его снова
|
||||
}
|
||||
|
||||
// Определяем тип поля и создаем соответствующий токен
|
||||
let fieldType = 'field';
|
||||
if (data.fieldType === 'universal') {
|
||||
fieldType = 'universal';
|
||||
} else if (data.fieldType === 'table') {
|
||||
fieldType = 'table';
|
||||
} else if (data.fieldType === 'table-row') {
|
||||
fieldType = 'table-row';
|
||||
} else if (data.fieldType === 'event') {
|
||||
fieldType = 'event';
|
||||
}
|
||||
|
||||
// Создаем токен
|
||||
const $token = createTriggerToken(fieldname, fieldType);
|
||||
|
||||
// Вставляем токен в нужную позицию
|
||||
if (position.element) {
|
||||
if (position.position === 'before') {
|
||||
$(position.element).before($token);
|
||||
$(position.element).before(' ');
|
||||
} else {
|
||||
$(position.element).after($token);
|
||||
$(position.element).after(' ');
|
||||
}
|
||||
} else {
|
||||
$container.append($token);
|
||||
$container.append(' ');
|
||||
}
|
||||
|
||||
// Инициализируем события для нового токена
|
||||
initTriggerTokenEvents($token);
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
function findDropPosition(x, y, $container) {
|
||||
let position = { element: null, position: 'after' };
|
||||
|
||||
// Проходим по всем дочерним элементам контейнера
|
||||
$container.contents().each(function() {
|
||||
if (this.nodeType === Node.TEXT_NODE) return;
|
||||
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
// Если курсор находится над элементом
|
||||
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
||||
position.element = this;
|
||||
|
||||
// Определяем, до или после элемента
|
||||
if (x < rect.left + rect.width / 2) {
|
||||
position.position = 'before';
|
||||
} else {
|
||||
position.position = 'after';
|
||||
}
|
||||
|
||||
return false; // Прерываем перебор
|
||||
}
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
function handleExistingTokenDrop(draggedElement, position, $container) {
|
||||
const $dragged = $(draggedElement);
|
||||
|
||||
if (position.element) {
|
||||
if (position.position === 'before') {
|
||||
$(position.element).before($dragged);
|
||||
$(position.element).before(' ');
|
||||
} else {
|
||||
$(position.element).after($dragged);
|
||||
$(position.element).after(' ');
|
||||
}
|
||||
} else {
|
||||
$container.append($dragged);
|
||||
$container.append(' ');
|
||||
}
|
||||
|
||||
window.draggedElement = null;
|
||||
}
|
||||
|
||||
function createFieldToken(data, isTableField) {
|
||||
let formulaText;
|
||||
|
||||
// Определяем тип поля и формулу
|
||||
const fieldInfo = determineFieldType(data.fieldname, data.group, isTableField);
|
||||
|
||||
if (fieldInfo.type === 'row') {
|
||||
formulaText = `row(${fieldInfo.fieldName})`;
|
||||
} else if (fieldInfo.type === 'sum') {
|
||||
formulaText = `sum(${fieldInfo.fieldName})`;
|
||||
} else {
|
||||
formulaText = `val(${fieldInfo.fieldName})`;
|
||||
}
|
||||
|
||||
return $(`<span class="formula-token field" data-value="${formulaText}" draggable="true" contenteditable="false">${formulaText}<span class="token-remove">×</span></span>`);
|
||||
}
|
||||
|
||||
function createOperatorToken(value) {
|
||||
return $(`<span class="formula-token operator" data-value="${value}" draggable="true" contenteditable="false">${value}<span class="token-remove">×</span></span>`);
|
||||
}
|
||||
|
||||
function insertElementAtPosition($element, position, $container) {
|
||||
if (position.element) {
|
||||
if (position.position === 'before') {
|
||||
$(position.element).before($element);
|
||||
$(position.element).before(' ');
|
||||
} else {
|
||||
$(position.element).after($element);
|
||||
$(position.element).after(' ');
|
||||
}
|
||||
} else {
|
||||
$container.append($element);
|
||||
$container.append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTokenEvents($token) {
|
||||
$token.find('.token-remove').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
$(this).parent().remove();
|
||||
});
|
||||
|
||||
$token.attr('draggable', 'true');
|
||||
|
||||
$token.off('dragstart').on('dragstart', function(e) {
|
||||
window.draggedElement = this;
|
||||
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'token',
|
||||
value: $(this).data('value'),
|
||||
html: $(this)[0].outerHTML
|
||||
}));
|
||||
|
||||
$(this).addClass('dragging');
|
||||
return true;
|
||||
});
|
||||
|
||||
$token.off('dragend').on('dragend', function(e) {
|
||||
$(this).removeClass('dragging');
|
||||
window.draggedElement = null;
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,477 @@
|
|||
function getFormulaText($container) {
|
||||
// Собираем все токены и текст в строку
|
||||
let formula = '';
|
||||
|
||||
$container.contents().each(function() {
|
||||
if (this.nodeType === Node.TEXT_NODE) {
|
||||
formula += this.textContent;
|
||||
} else {
|
||||
const $elem = $(this);
|
||||
if ($elem.hasClass('formula-token')) {
|
||||
formula += $elem.data('value');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем лишние пробелы
|
||||
return formula.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function parseAndRenderFormula($container, formula, isTableField) {
|
||||
if (!formula) return;
|
||||
|
||||
// Регулярные выражения для поиска функций и операторов
|
||||
const funcRegex = /(val|row|sum|univ|report|table)\(([^)]+)\)/g;
|
||||
const operatorsRegex = /([+\-*/()%])/g;
|
||||
|
||||
// Сохраняем текущую позицию
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
let formulaParts = [];
|
||||
|
||||
// Поиск всех функций
|
||||
while ((match = funcRegex.exec(formula)) !== null) {
|
||||
// Добавляем текст до функции
|
||||
if (match.index > lastIndex) {
|
||||
formulaParts.push({
|
||||
type: 'text',
|
||||
value: formula.substring(lastIndex, match.index)
|
||||
});
|
||||
}
|
||||
|
||||
// Добавляем функцию
|
||||
formulaParts.push({
|
||||
type: 'function',
|
||||
value: match[0],
|
||||
func: match[1],
|
||||
param: match[2]
|
||||
});
|
||||
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
// Добавляем оставшийся текст после последней функции
|
||||
if (lastIndex < formula.length) {
|
||||
formulaParts.push({
|
||||
type: 'text',
|
||||
value: formula.substring(lastIndex)
|
||||
});
|
||||
}
|
||||
|
||||
// Разбираем операторы в частях, которые не являются функциями
|
||||
let finalParts = [];
|
||||
formulaParts.forEach(part => {
|
||||
if (part.type === 'function') {
|
||||
finalParts.push(part);
|
||||
} else if (part.type === 'text') {
|
||||
let text = part.value;
|
||||
lastIndex = 0;
|
||||
|
||||
// Сбрасываем lastIndex для operatorsRegex
|
||||
operatorsRegex.lastIndex = 0;
|
||||
|
||||
// Разбираем операторы
|
||||
while ((match = operatorsRegex.exec(text)) !== null) {
|
||||
// Добавляем текст до оператора
|
||||
if (match.index > lastIndex) {
|
||||
const textBefore = text.substring(lastIndex, match.index).trim();
|
||||
if (textBefore) {
|
||||
finalParts.push({
|
||||
type: 'text',
|
||||
value: textBefore
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем оператор
|
||||
finalParts.push({
|
||||
type: 'operator',
|
||||
value: match[1]
|
||||
});
|
||||
|
||||
lastIndex = match.index + match[1].length;
|
||||
}
|
||||
|
||||
// Добавляем оставшийся текст после последнего оператора
|
||||
if (lastIndex < text.length) {
|
||||
const textAfter = text.substring(lastIndex).trim();
|
||||
if (textAfter) {
|
||||
finalParts.push({
|
||||
type: 'text',
|
||||
value: textAfter
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Рендерим части формулы
|
||||
finalParts.forEach(part => {
|
||||
if (part.type === 'function') {
|
||||
// Определяем класс в зависимости от типа функции
|
||||
let tokenClass = 'field';
|
||||
if (part.func === 'univ') {
|
||||
tokenClass = 'field universal-field';
|
||||
} else if (part.func === 'report') {
|
||||
tokenClass = 'field report-field';
|
||||
} else if (part.func === 'table') {
|
||||
tokenClass = 'field table-field';
|
||||
}
|
||||
|
||||
const $token = $(`<span class="formula-token ${tokenClass}" data-value="${part.value}" contenteditable="false" draggable="true">${part.value}<span class="token-remove">×</span></span>`);
|
||||
$container.append($token).append(' ');
|
||||
} else if (part.type === 'operator') {
|
||||
const $token = $(`<span class="formula-token operator" data-value="${part.value}" contenteditable="false" draggable="true">${part.value}<span class="token-remove">×</span></span>`);
|
||||
$container.append($token).append(' ');
|
||||
} else if (part.type === 'text') {
|
||||
$container.append(document.createTextNode(`${part.value} `));
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем обработчики для токенов
|
||||
addTokenHandlers($container);
|
||||
|
||||
return formula;
|
||||
}
|
||||
|
||||
function addTokenHandlers($container) {
|
||||
// Удаление токенов
|
||||
$container.find('.token-remove').off('click').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
$(this).parent().remove();
|
||||
});
|
||||
|
||||
// Перетаскивание токенов
|
||||
$container.find('.formula-token').each(function() {
|
||||
$(this).attr('draggable', 'true');
|
||||
|
||||
$(this).off('dragstart').on('dragstart', function(e) {
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'token',
|
||||
value: $(this).data('value'),
|
||||
html: $(this)[0].outerHTML
|
||||
}));
|
||||
window.draggedElement = this;
|
||||
});
|
||||
|
||||
$(this).off('dragend').on('dragend', function() {
|
||||
window.draggedElement = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addTokenToFormula(token, type, $resultContainer, isTableField = false, group = null, frm = null) {
|
||||
if (type === 'field') {
|
||||
let formulaText;
|
||||
|
||||
// Определяем тип поля и формулу
|
||||
const fieldInfo = determineFieldType(token, group, isTableField);
|
||||
|
||||
if (fieldInfo.type === 'table_row') {
|
||||
// Для полей конкретных строк таблиц
|
||||
formulaText = `val(${fieldInfo.fullFieldName})`;
|
||||
} else if (fieldInfo.type === 'row') {
|
||||
formulaText = `row(${fieldInfo.fieldName})`;
|
||||
} else if (fieldInfo.type === 'sum') {
|
||||
let tableName = fieldInfo.tableName || token.match(/^([a-z_]+)/)[1];
|
||||
let fieldWithoutPrefix = token.substring(tableName.length);
|
||||
if (fieldWithoutPrefix.startsWith('_')) {
|
||||
fieldWithoutPrefix = fieldWithoutPrefix.substring(1);
|
||||
}
|
||||
|
||||
formulaText = `sum(${tableName}_${fieldWithoutPrefix})`;
|
||||
} else {
|
||||
formulaText = `val(${fieldInfo.fieldName})`;
|
||||
}
|
||||
|
||||
const $token = $(`<span class="formula-token field" data-value="${formulaText}" contenteditable="false" draggable="true">${formulaText}<span class="token-remove">×</span></span>`);
|
||||
$resultContainer.append($token).append(' ');
|
||||
} else if (type === 'operator') {
|
||||
const $token = $(`<span class="formula-token operator" data-value="${token}" contenteditable="false" draggable="true">${token}<span class="token-remove">×</span></span>`);
|
||||
$resultContainer.append($token).append(' ');
|
||||
} else if (type === 'universal-field') {
|
||||
// Обработка для универсальных полей
|
||||
const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}");
|
||||
const field = universalFields[token];
|
||||
if (field) {
|
||||
const formulaText = `univ(${field.name})`;
|
||||
const $token = $(`<span class="formula-token field universal-field" data-value="${formulaText}" contenteditable="false" draggable="true">${formulaText}<span class="token-remove">×</span></span>`);
|
||||
$resultContainer.append($token).append(' ');
|
||||
}
|
||||
} else if (type === 'report-field') {
|
||||
// Обработка для полей отчетов
|
||||
const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
|
||||
const field = reportFields[token];
|
||||
if (field) {
|
||||
const formulaText = `report(${field.name})`;
|
||||
const $token = $(`<span class="formula-token field report-field" data-value="${formulaText}" contenteditable="false" draggable="true">${formulaText}<span class="token-remove">×</span></span>`);
|
||||
$resultContainer.append($token).append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
// Переносим курсор в конец контейнера
|
||||
setEndOfContentEditable($resultContainer[0]);
|
||||
|
||||
// Добавляем обработчики для токенов
|
||||
addTokenHandlers($resultContainer);
|
||||
}
|
||||
|
||||
function determineFieldType(fieldName, group, isInTableField) {
|
||||
// Проверяем, является ли поле ссылкой на строку табличной части
|
||||
const rowPattern = /^([a-z_]+)_row_(\d+)_([a-z_]+)$/i;
|
||||
const rowMatch = fieldName.match(rowPattern);
|
||||
|
||||
if (rowMatch) {
|
||||
const tableName = rowMatch[1];
|
||||
const rowIndex = rowMatch[2];
|
||||
const fieldNameInRow = rowMatch[3];
|
||||
|
||||
return {
|
||||
type: 'table_row',
|
||||
tableName: tableName,
|
||||
rowIndex: rowIndex,
|
||||
fieldName: fieldNameInRow,
|
||||
fullFieldName: fieldName
|
||||
};
|
||||
}
|
||||
|
||||
// Получаем части имени поля
|
||||
// Проверяем существование имен групп "items", "taxes" и т.д. без подчеркивания
|
||||
const tablePrefixes = ["items", "taxes", "packed_items", "payments", "advances", "timesheets", "payment_schedule", "sales_team"];
|
||||
|
||||
// Проверяем, начинается ли fieldName с известного префикса таблицы
|
||||
let isTableField = false;
|
||||
let tableName = "";
|
||||
|
||||
for (const prefix of tablePrefixes) {
|
||||
if (fieldName.startsWith(prefix)) {
|
||||
// Это поле табличной части без подчеркивания (например, "itemsqty")
|
||||
isTableField = true;
|
||||
tableName = prefix;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Если не нашли по префиксу, проверяем по первой части с подчеркиванием
|
||||
if (!isTableField) {
|
||||
const parts = fieldName.split('_');
|
||||
if (parts.length > 1) {
|
||||
const potentialTableName = parts[0];
|
||||
if (tablePrefixes.includes(potentialTableName)) {
|
||||
isTableField = true;
|
||||
tableName = potentialTableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Определяем тип формулы
|
||||
if (isTableField) {
|
||||
// Для полей таблицы
|
||||
if (isInTableField) {
|
||||
// В табличной части используем row()
|
||||
// Извлекаем имя поля без префикса таблицы
|
||||
let fieldNameWithoutTable = fieldName;
|
||||
if (fieldName.startsWith(tableName)) {
|
||||
fieldNameWithoutTable = fieldName.substring(tableName.length);
|
||||
// Если есть подчеркивание, удаляем его
|
||||
if (fieldNameWithoutTable.startsWith('_')) {
|
||||
fieldNameWithoutTable = fieldNameWithoutTable.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'row',
|
||||
fieldName: fieldNameWithoutTable
|
||||
};
|
||||
} else {
|
||||
// В основном документе используем sum()
|
||||
return {
|
||||
type: 'sum',
|
||||
fieldName: fieldName,
|
||||
tableName: tableName
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Для обычных полей всегда val()
|
||||
return {
|
||||
type: 'val',
|
||||
fieldName: fieldName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function openFormulaBuilder(frm, targetFieldname, isTableField, currentFormula, fields, grouped_fields) {
|
||||
const $modal = $("#formula_builder_modal");
|
||||
const $resultContainer = $modal.find(".formula-result");
|
||||
|
||||
// Очистка предыдущих обработчиков и данных
|
||||
$modal.off('click', '.field-btn');
|
||||
$modal.off('click', '.universal-field-btn');
|
||||
$modal.off('click', '.report-field-btn');
|
||||
$modal.off('click', '.operator-btn');
|
||||
$modal.find('.apply-formula').off('click');
|
||||
$resultContainer.empty();
|
||||
$modal.find(".field-groups-container").empty();
|
||||
|
||||
// Заполняем текущую формулу, если она есть
|
||||
if (currentFormula) {
|
||||
parseAndRenderFormula($resultContainer, currentFormula, isTableField);
|
||||
}
|
||||
|
||||
// Заполняем панель с полями
|
||||
populateFieldsPanel($modal, frm, grouped_fields, isTableField);
|
||||
|
||||
// Сохраняем данные о текущем поле
|
||||
$modal.data("target-field", targetFieldname);
|
||||
$modal.data("is-table-field", isTableField);
|
||||
|
||||
// Обработчики для кнопок операторов
|
||||
$modal.find('.operator-btn').off('click').on('click', function() {
|
||||
const operator = $(this).data('operator');
|
||||
addTokenToFormula(operator, 'operator', $resultContainer);
|
||||
});
|
||||
|
||||
// Обработчик для кнопки применения формулы
|
||||
$modal.find('.apply-formula').off('click').on('click', function() {
|
||||
const formula = getFormulaText($resultContainer);
|
||||
const $targetInput = $(frm.wrapper).find(`.formula-input[data-fieldname="${targetFieldname}"]`);
|
||||
$targetInput.val(formula);
|
||||
$modal.modal('hide');
|
||||
});
|
||||
|
||||
// Обработчики для кнопок полей - используем делегирование событий
|
||||
$modal.off('click', '.field-btn').on('click', '.field-btn', function() {
|
||||
const fieldname = $(this).data('fieldname');
|
||||
addTokenToFormula(fieldname, 'field', $resultContainer, isTableField, $(this).data('group'));
|
||||
});
|
||||
|
||||
// Обработчики для кнопок универсальных полей
|
||||
$modal.off('click', '.universal-field-btn').on('click', '.universal-field-btn', function() {
|
||||
const fieldId = $(this).data('field-id');
|
||||
const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}");
|
||||
const field = universalFields[fieldId];
|
||||
|
||||
if (field) {
|
||||
const formulaText = `univ(${field.name})`;
|
||||
const $token = $(`<span class="formula-token field universal-field" data-value="${formulaText}" contenteditable="false" draggable="true">${formulaText}<span class="token-remove">×</span></span>`);
|
||||
$resultContainer.append($token).append(' ');
|
||||
|
||||
// Добавляем обработчики для токенов
|
||||
addTokenHandlers($resultContainer);
|
||||
|
||||
// Переносим курсор в конец контейнера
|
||||
setEndOfContentEditable($resultContainer[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчики для кнопок полей отчетов
|
||||
$modal.off('click', '.report-field-btn').on('click', '.report-field-btn', function() {
|
||||
const fieldId = $(this).data('field-id');
|
||||
const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
|
||||
const field = reportFields[fieldId];
|
||||
|
||||
if (field) {
|
||||
const formulaText = `report(${field.name})`;
|
||||
const $token = $(`<span class="formula-token field report-field" data-value="${formulaText}" contenteditable="false" draggable="true">${formulaText}<span class="token-remove">×</span></span>`);
|
||||
$resultContainer.append($token).append(' ');
|
||||
|
||||
// Добавляем обработчики для токенов
|
||||
addTokenHandlers($resultContainer);
|
||||
|
||||
// Переносим курсор в конец контейнера
|
||||
setEndOfContentEditable($resultContainer[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Показываем модальное окно
|
||||
$modal.modal('show');
|
||||
|
||||
// Инициализируем перетаскивание
|
||||
initDragAndDrop($modal, $resultContainer, isTableField, frm);
|
||||
|
||||
// Настраиваем редактируемый контейнер
|
||||
setupEditableContainer($resultContainer);
|
||||
|
||||
// Фокусируемся на редактируемом поле
|
||||
$resultContainer.focus();
|
||||
}
|
||||
|
||||
function setupEditableContainer($container) {
|
||||
// Обработчик нажатия клавиш в контейнере формулы
|
||||
$container.off('keydown').on('keydown', function(e) {
|
||||
// Если нажата клавиша Delete или Backspace
|
||||
if (e.keyCode === 8 || e.keyCode === 46) {
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) {
|
||||
// Текстовый узел - обычное удаление
|
||||
return true;
|
||||
} else {
|
||||
// Возможно, выделен токен
|
||||
const $selectedNode = $(range.commonAncestorContainer);
|
||||
|
||||
if ($selectedNode.hasClass('formula-token') || $selectedNode.parents('.formula-token').length) {
|
||||
// Удаляем токен
|
||||
const $token = $selectedNode.hasClass('formula-token') ?
|
||||
$selectedNode : $selectedNode.parents('.formula-token');
|
||||
$token.remove();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Обработчик вставки текста
|
||||
$container.off('paste').on('paste', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Получаем вставляемый текст
|
||||
let text = '';
|
||||
if (e.clipboardData || e.originalEvent.clipboardData) {
|
||||
text = (e.clipboardData || e.originalEvent.clipboardData).getData('text/plain');
|
||||
} else if (window.clipboardData) {
|
||||
text = window.clipboardData.getData('Text');
|
||||
}
|
||||
|
||||
// Вставляем текст как обычный текст
|
||||
if (text) {
|
||||
document.execCommand('insertText', false, text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupTokenDragging($container) {
|
||||
$container.find('.formula-token').each(function() {
|
||||
$(this).attr('draggable', 'true');
|
||||
|
||||
$(this).off('dragstart').on('dragstart', function(e) {
|
||||
// Сохраняем ссылку на текущий элемент
|
||||
window.draggedElement = this;
|
||||
|
||||
// Добавляем данные для переноса
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'token',
|
||||
value: $(this).data('value'),
|
||||
html: $(this)[0].outerHTML
|
||||
}));
|
||||
|
||||
// Добавляем класс перетаскивания
|
||||
$(this).addClass('dragging');
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// При завершении перетаскивания
|
||||
$(this).off('dragend').on('dragend', function(e) {
|
||||
$(this).removeClass('dragging');
|
||||
window.draggedElement = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,460 @@
|
|||
function update_or_create_client_script(target_doctype, script_code) {
|
||||
// Имя для нашего клиентского скрипта формул
|
||||
const formula_script_name = `Formula_Editor_Script_for_${target_doctype}`;
|
||||
|
||||
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) {
|
||||
if (!doctype) {
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.client.get_list",
|
||||
args: {
|
||||
doctype: "Client Script",
|
||||
filters: { dt: doctype },
|
||||
fields: ["name"]
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.length > 0) {
|
||||
const script_names = r.message.map(script => script.name);
|
||||
|
||||
// Удаляем каждый найденный скрипт
|
||||
script_names.forEach(script_name => {
|
||||
frappe.call({
|
||||
method: "frappe.client.delete",
|
||||
args: {
|
||||
doctype: "Client Script",
|
||||
name: script_name
|
||||
},
|
||||
callback: function(response) {
|
||||
if (response.exc) {
|
||||
} else {
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function generateTransformExpressionFunction(universal_fields, report_fields) {
|
||||
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() ===
|
||||
const tablePrefixes = ["items", "taxes", "packed_items", "payments", "advances", "timesheets", "payment_schedule", "sales_team"];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,689 @@
|
|||
function createModals() {
|
||||
createFormulaBuilderModal();
|
||||
createTriggerFieldsModal();
|
||||
createUniversalFieldsModal();
|
||||
createReportFieldModal();
|
||||
createTableRowFormulaModal();
|
||||
createTableRowFieldSelectorModal();
|
||||
createTableFieldSelectorModal();
|
||||
}
|
||||
|
||||
function createFormulaBuilderModal() {
|
||||
if ($("#formula_builder_modal").length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalHtml = `
|
||||
<div id="formula_builder_modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Визуальный редактор формул</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="formula-workspace">
|
||||
<div class="formula-result-container">
|
||||
<div class="formula-result-title">Формула:</div>
|
||||
<div class="formula-result" contenteditable="true"></div>
|
||||
</div>
|
||||
<div class="operators-panel">
|
||||
<div class="operators-title">Операторы:</div>
|
||||
<div class="operators-container">
|
||||
<button class="btn btn-sm operator-btn" data-operator="+">+</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="-">-</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="*">*</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="/">/</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="%">%</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="(">(</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator=")">)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fields-panel">
|
||||
<div class="fields-title">Поля:</div>
|
||||
<div class="field-groups-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary apply-formula">Применить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Стили с поддержкой перетаскивания
|
||||
const modalStyles = `
|
||||
<style>
|
||||
.formula-workspace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.formula-result-container, .trigger-result-container {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.formula-result-title, .trigger-result-title, .fields-title, .operators-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.formula-result, .trigger-result {
|
||||
min-height: 30px;
|
||||
padding: 5px;
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.formula-result.drag-hover, .trigger-result.drag-hover {
|
||||
background-color: #f0f8ff;
|
||||
border: 1px dashed #4285f4;
|
||||
}
|
||||
|
||||
.operators-panel, .fields-panel {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.universal-field {
|
||||
background-color: #e3efff !important;
|
||||
border: 1px solid #abc9f5 !important;
|
||||
}
|
||||
|
||||
.operators-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.field-groups-container, .trigger-field-groups-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.field-group, .trigger-field-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.field-group-title, .trigger-field-group-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.field-buttons-container, .trigger-field-buttons-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.field-btn, .operator-btn, .trigger-field-btn {
|
||||
margin: 3px;
|
||||
background-color: #f0f4f8;
|
||||
border: 1px solid #d0d8e0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.field-btn:hover, .operator-btn:hover, .trigger-field-btn:hover {
|
||||
background-color: #e0e8f0;
|
||||
}
|
||||
|
||||
.formula-token, .trigger-token {
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
margin: 2px;
|
||||
border-radius: 3px;
|
||||
cursor: move;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.formula-token.field, .trigger-token.field {
|
||||
background-color: #e3efff;
|
||||
border: 1px solid #abc9f5;
|
||||
}
|
||||
|
||||
.formula-token.operator {
|
||||
background-color: #ffe8e3;
|
||||
border: 1px solid #f5c3b9;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.formula-token:hover, .trigger-token:hover {
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.formula-token.dragging, .trigger-token.dragging {
|
||||
opacity: 0.7;
|
||||
box-shadow: 0 0 5px rgba(0, 100, 255, 0.7);
|
||||
}
|
||||
|
||||
.formula-token .token-remove, .trigger-token .token-remove {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #ff5555;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.formula-token:hover .token-remove, .trigger-token:hover .token-remove {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drop-marker, .trigger-drop-marker {
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
height: 18px;
|
||||
background-color: #4285f4;
|
||||
vertical-align: middle;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.draggable-item {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
/* Стили для универсальных полей */
|
||||
.universal-field-form {
|
||||
padding: 10px;
|
||||
}
|
||||
.filter-row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.universal-field-token {
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
margin: 2px;
|
||||
border-radius: 3px;
|
||||
cursor: move;
|
||||
background-color: #eff8e6;
|
||||
border: 1px solid #b7d69d;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
.universal-field-token .token-remove {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #ff5555;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.universal-field-token:hover .token-remove {
|
||||
display: block;
|
||||
}
|
||||
.universal-field-btn {
|
||||
margin: 3px;
|
||||
background-color: #eff8e6;
|
||||
border: 1px solid #b7d69d;
|
||||
cursor: pointer;
|
||||
}
|
||||
.universal-field-btn:hover {
|
||||
background-color: #deedcf;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
$('body').append(modalHtml);
|
||||
$('head').append(modalStyles);
|
||||
}
|
||||
|
||||
function createTriggerFieldsModal() {
|
||||
if ($("#trigger_fields_modal").length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalHtml = `
|
||||
<div id="trigger_fields_modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Редактор полей изменения</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="trigger-workspace">
|
||||
<div class="trigger-result-container">
|
||||
<div class="trigger-result-title">Поля изменения:</div>
|
||||
<div class="trigger-result" contenteditable="true"></div>
|
||||
</div>
|
||||
<div class="fields-panel">
|
||||
<div class="fields-title">Доступные поля:</div>
|
||||
<div class="trigger-field-groups-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary apply-triggers">Применить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('body').append(modalHtml);
|
||||
}
|
||||
|
||||
function createUniversalFieldsModal() {
|
||||
if ($("#universal_fields_modal").length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalHtml = `
|
||||
<div id="universal_fields_modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Создание универсального поля</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="universal-field-form"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary save-universal-field" disabled>Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
$('body').append(modalHtml);
|
||||
}
|
||||
|
||||
function createReportFieldModal() {
|
||||
if ($("#report_fields_modal").length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalHtml = `
|
||||
<div id="report_fields_modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Создание поля отчета</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="report-field-form"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary save-report-field" disabled>Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
$('body').append(modalHtml);
|
||||
}
|
||||
|
||||
function createTableRowFormulaModal() {
|
||||
if ($("#table_row_formula_modal").length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalHtml = `
|
||||
<div id="table_row_formula_modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Формула для строки табличной части</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<label for="table_row_table">Табличная часть:</label>
|
||||
<select id="table_row_table" class="form-control"></select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="table_row_field">Поле:</label>
|
||||
<select id="table_row_field" class="form-control" disabled></select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="table_row_index">Номер строки:</label>
|
||||
<input type="number" id="table_row_index" class="form-control" min="1" value="1">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="table_row_order">Порядок выполнения:</label>
|
||||
<input type="number" id="table_row_order" class="form-control" min="0" value="10">
|
||||
</div>
|
||||
</div>
|
||||
<div class="formula-workspace">
|
||||
<div class="formula-result-container">
|
||||
<div class="formula-result-title">Формула:</div>
|
||||
<div class="formula-result table-row-formula" contenteditable="true"></div>
|
||||
</div>
|
||||
<div class="operators-panel">
|
||||
<div class="operators-title">Операторы:</div>
|
||||
<div class="operators-container">
|
||||
<button class="btn btn-sm operator-btn" data-operator="+">+</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="-">-</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="*">*</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="/">/</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="%">%</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator="(">(</button>
|
||||
<button class="btn btn-sm operator-btn" data-operator=")">)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fields-panel">
|
||||
<div class="fields-title">Поля:</div>
|
||||
<div class="field-groups-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="triggers-container mt-4">
|
||||
<div class="trigger-result-title">Обновлять при изменении:</div>
|
||||
<div class="trigger-result table-row-trigger" contenteditable="true"></div>
|
||||
<div class="trigger-actions mt-2">
|
||||
<button class="btn btn-sm btn-info add-predefined-event">
|
||||
<i class="fa fa-calendar"></i> При открытии документа
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm open-trigger-editor-in-row">
|
||||
<i class="fa fa-list"></i> Выбрать поле
|
||||
</button>
|
||||
<button class="btn btn-warning btn-sm select-table-row-trigger-field">
|
||||
<i class="fa fa-plus"></i> Выбрать поле строки т.ч.
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary save-table-row-formula">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('body').append(modalHtml);
|
||||
|
||||
// Обработчики для селекторов
|
||||
$("#table_row_table").on('change', function() {
|
||||
const selectedTable = $(this).val();
|
||||
if (selectedTable) {
|
||||
loadTableFieldsForSelector(selectedTable);
|
||||
$("#table_row_field").prop('disabled', false);
|
||||
} else {
|
||||
$("#table_row_field").prop('disabled', true).empty().append('<option value="">Выберите поле</option>');
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем обработчик для кнопки "При открытии документа"
|
||||
$("#table_row_formula_modal").on('click', '.add-predefined-event', function() {
|
||||
const $triggerContainer = $("#table_row_formula_modal").find(".table-row-trigger");
|
||||
// Используем специальную функцию для добавления предопределенного события
|
||||
addTokenToTriggers("При открытии документа", $triggerContainer, 'event');
|
||||
});
|
||||
|
||||
// Добавляем обработчик для кнопки "Выбрать поле"
|
||||
$("#table_row_formula_modal").on('click', '.open-trigger-editor-in-row', function() {
|
||||
const $triggerContainer = $("#table_row_formula_modal").find(".table-row-trigger");
|
||||
const frm = cur_frm;
|
||||
|
||||
// Открываем стандартный редактор триггеров, используя контейнер из модального окна строки таблицы
|
||||
openTriggerFieldsEditorForTableRow(frm, $triggerContainer);
|
||||
});
|
||||
}
|
||||
|
||||
function createTableRowFieldSelectorModal() {
|
||||
if ($("#table_row_field_selector_modal").length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalHtml = `
|
||||
<div id="table_row_field_selector_modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Выбор поля строки табличной части</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="row_field_table">Табличная часть:</label>
|
||||
<select id="row_field_table" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="row_field_name">Поле:</label>
|
||||
<select id="row_field_name" class="form-control" disabled></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="row_field_index">Номер строки:</label>
|
||||
<input type="number" id="row_field_index" class="form-control" min="1" value="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary add-table-row-field">Добавить поле</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('body').append(modalHtml);
|
||||
|
||||
// Обработчики для селекторов
|
||||
$("#row_field_table").on('change', function() {
|
||||
const selectedTable = $(this).val();
|
||||
if (selectedTable) {
|
||||
loadTableFieldsForSelector(selectedTable);
|
||||
$("#row_field_name").prop('disabled', false);
|
||||
} else {
|
||||
$("#row_field_name").prop('disabled', true).empty().append('<option value="">Выберите поле</option>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createTableFieldSelectorModal() {
|
||||
if ($("#table_field_selector_modal").length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalHtml = `
|
||||
<div id="table_field_selector_modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Выбор поля из строки табличной части</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="table_field_table">Табличная часть:</label>
|
||||
<select id="table_field_table" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="table_field_name">Поле:</label>
|
||||
<select id="table_field_name" class="form-control" disabled></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="table_field_row">Номер строки:</label>
|
||||
<input type="number" id="table_field_row" class="form-control" min="1" value="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary add-table-field">Добавить поле</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('body').append(modalHtml);
|
||||
|
||||
// Обработчики для селекторов
|
||||
$("#table_field_table").on('change', function() {
|
||||
const selectedTable = $(this).val();
|
||||
if (selectedTable) {
|
||||
loadTableFieldsForTableSelector(selectedTable);
|
||||
$("#table_field_name").prop('disabled', false);
|
||||
} else {
|
||||
$("#table_field_name").prop('disabled', true).empty().append('<option value="">Выберите поле</option>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadTableFieldsForTableSelector(tableName) {
|
||||
const $fieldSelect = $("#table_field_name");
|
||||
$fieldSelect.empty().append('<option value="">Выберите поле</option>');
|
||||
|
||||
const tableDoctypes = $("#table_field_selector_modal").data("table-doctypes") || {};
|
||||
const doctype = tableDoctypes[tableName];
|
||||
|
||||
if (!doctype) return;
|
||||
|
||||
// Получаем поля DocType табличной части
|
||||
frappe.model.with_doctype(doctype, function() {
|
||||
const meta = frappe.get_meta(doctype);
|
||||
if (!meta) return;
|
||||
|
||||
// Добавляем все поля, которые могут быть использованы в формулах
|
||||
const validTypes = ['Int', 'Float', 'Currency', 'Data', 'Text', 'Date', 'Datetime', 'Check', 'Link', 'Select', 'Percent'];
|
||||
meta.fields.forEach(field => {
|
||||
if (field.fieldname && validTypes.includes(field.fieldtype)) {
|
||||
$fieldSelect.append(`<option value="${field.fieldname}" data-fieldtype="${field.fieldtype}">${field.label || field.fieldname}</option>`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadTablesListForTableSelector(frm) {
|
||||
const $tableSelect = $("#table_field_table");
|
||||
$tableSelect.empty().append('<option value="">Выберите табличную часть</option>');
|
||||
|
||||
// Получаем все табличные части DocType
|
||||
frappe.model.with_doctype(frm.doc.target_doctype, function() {
|
||||
const meta = frappe.get_meta(frm.doc.target_doctype);
|
||||
if (!meta) return;
|
||||
|
||||
const tableDoctypes = {};
|
||||
|
||||
meta.fields.forEach(field => {
|
||||
if (field.fieldtype === 'Table' && field.options) {
|
||||
$tableSelect.append(`<option value="${field.fieldname}" data-doctype="${field.options}">${field.label || field.fieldname}</option>`);
|
||||
tableDoctypes[field.fieldname] = field.options;
|
||||
}
|
||||
});
|
||||
|
||||
// Сохраняем информацию о DocType для табличных частей
|
||||
$("#table_field_selector_modal").data("table-doctypes", tableDoctypes);
|
||||
});
|
||||
}
|
||||
|
||||
function openTableFieldSelector(frm, targetContainer, isForTrigger = false) {
|
||||
const $modal = $("#table_field_selector_modal");
|
||||
|
||||
// Сбрасываем форму
|
||||
$("#table_field_table").val('');
|
||||
$("#table_field_name").empty().append('<option value="">Выберите поле</option>').prop('disabled', true);
|
||||
$("#table_field_row").val(1);
|
||||
|
||||
// Удаляем старые обработчики
|
||||
$modal.find('.add-table-field').off('click');
|
||||
|
||||
// Загружаем список табличных частей
|
||||
loadTablesListForTableSelector(frm);
|
||||
|
||||
// Сохраняем информацию о целевом контейнере и типе (формула или триггер)
|
||||
$modal.data("target-container", targetContainer);
|
||||
$modal.data("is-trigger", isForTrigger);
|
||||
$modal.data("frm", frm);
|
||||
|
||||
// Обработчик для кнопки добавления
|
||||
$modal.find('.add-table-field').on('click', function() {
|
||||
const tableName = $("#table_field_table").val();
|
||||
const fieldName = $("#table_field_name").val();
|
||||
const rowNumber = $("#table_field_row").val();
|
||||
|
||||
if (!tableName || !fieldName || !rowNumber) {
|
||||
frappe.msgprint({
|
||||
title: __('Внимание'),
|
||||
message: __('Выберите табличную часть, поле и укажите номер строки'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const $container = $($modal.data("target-container"));
|
||||
const isForTrigger = $modal.data("is-trigger");
|
||||
|
||||
if (isForTrigger) {
|
||||
// Для триггеров добавляем идентификатор поля
|
||||
const fieldId = `${tableName}_${fieldName}`;
|
||||
addTokenToTriggers(fieldId, $container, 'table-field');
|
||||
} else {
|
||||
// Для формул добавляем вызов функции table()
|
||||
const formulaText = `table(${tableName}, ${rowNumber}, ${fieldName})`;
|
||||
const $token = $(`<span class="formula-token field table-field" data-value="${formulaText}" contenteditable="false" draggable="true">${formulaText}<span class="token-remove">×</span></span>`);
|
||||
$container.append($token).append(' ');
|
||||
|
||||
// Добавляем обработчики для токенов
|
||||
addTokenHandlers($container);
|
||||
|
||||
// Переносим курсор в конец контейнера
|
||||
setEndOfContentEditable($container[0]);
|
||||
}
|
||||
|
||||
$modal.modal('hide');
|
||||
});
|
||||
|
||||
// Показываем модальное окно
|
||||
$modal.modal('show');
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,418 @@
|
|||
function parseAndRenderTriggers($container, triggers) {
|
||||
if (!triggers) return;
|
||||
|
||||
// Разбиваем строку с триггерами по запятым
|
||||
const triggerFields = triggers.split(',').map(field => field.trim()).filter(field => field);
|
||||
|
||||
// Создаем токены для каждого поля
|
||||
triggerFields.forEach(field => {
|
||||
// Определяем тип триггера
|
||||
let fieldType = 'field';
|
||||
|
||||
if (isPredefinedEvent(field)) {
|
||||
fieldType = 'event';
|
||||
} else if (field.startsWith('univ(')) {
|
||||
fieldType = 'universal';
|
||||
} else if (field.includes('_') && !field.includes('_row_')) {
|
||||
// Проверяем, является ли это полем табличной части
|
||||
const parts = field.split('_');
|
||||
if (parts.length >= 2) {
|
||||
fieldType = 'table';
|
||||
}
|
||||
} else if (field.includes('_row_')) {
|
||||
fieldType = 'table-row';
|
||||
}
|
||||
|
||||
addTokenToTriggers(field, $container, fieldType);
|
||||
});
|
||||
}
|
||||
|
||||
function getTriggerFieldsText($container) {
|
||||
// Собираем значения всех токенов
|
||||
const triggerFields = [];
|
||||
|
||||
$container.find('.trigger-token').each(function() {
|
||||
triggerFields.push($(this).data('value'));
|
||||
});
|
||||
|
||||
// Объединяем их через запятую
|
||||
return triggerFields.join(', ');
|
||||
}
|
||||
|
||||
function addTokenToTriggers(fieldname, $resultContainer, fieldType = 'field') {
|
||||
// Проверяем, не добавлено ли уже это поле
|
||||
const existingTokens = $resultContainer.find(`.trigger-token[data-value="${fieldname}"]`);
|
||||
if (existingTokens.length > 0) {
|
||||
return; // Если поле уже добавлено, не добавляем его снова
|
||||
}
|
||||
|
||||
// Определяем класс токена в зависимости от типа поля
|
||||
let tokenClass = "trigger-token field";
|
||||
|
||||
switch (fieldType) {
|
||||
case 'event':
|
||||
tokenClass = "trigger-token field highlight-event";
|
||||
break;
|
||||
case 'universal':
|
||||
tokenClass = "trigger-token field universal-field";
|
||||
break;
|
||||
case 'table':
|
||||
tokenClass = "trigger-token field table-field";
|
||||
break;
|
||||
case 'table-row':
|
||||
tokenClass = "trigger-token field table-row-field";
|
||||
break;
|
||||
default:
|
||||
tokenClass = "trigger-token field";
|
||||
}
|
||||
|
||||
const $token = $(`<span class="${tokenClass}" data-value="${fieldname}" contenteditable="false">${fieldname}<span class="token-remove">×</span></span>`);
|
||||
$resultContainer.append($token).append(' ');
|
||||
|
||||
// Переносим курсор в конец контейнера
|
||||
setEndOfContentEditable($resultContainer[0]);
|
||||
|
||||
// Добавляем обработчики для токенов
|
||||
attachTriggerTokenEvents($resultContainer);
|
||||
}
|
||||
|
||||
function attachTriggerTokenEvents($container) {
|
||||
// Удаление токена
|
||||
$container.find('.token-remove').off('click').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
$(this).parent().remove();
|
||||
});
|
||||
|
||||
// Перетаскивание токенов
|
||||
$container.find('.trigger-token').each(function() {
|
||||
$(this).attr('draggable', 'true');
|
||||
|
||||
$(this).off('dragstart').on('dragstart', function(e) {
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'trigger-token',
|
||||
value: $(this).data('value'),
|
||||
html: $(this)[0].outerHTML,
|
||||
tokenClass: $(this).attr('class')
|
||||
}));
|
||||
window.draggedTriggerElement = this;
|
||||
});
|
||||
|
||||
$(this).off('dragend').on('dragend', function() {
|
||||
window.draggedTriggerElement = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createTriggerToken(fieldname, fieldType) {
|
||||
// Определяем класс токена в зависимости от типа поля
|
||||
let tokenClass = "trigger-token field";
|
||||
|
||||
switch (fieldType) {
|
||||
case 'event':
|
||||
tokenClass = "trigger-token field highlight-event";
|
||||
break;
|
||||
case 'universal':
|
||||
tokenClass = "trigger-token field universal-field";
|
||||
break;
|
||||
case 'table':
|
||||
tokenClass = "trigger-token field table-field";
|
||||
break;
|
||||
case 'table-row':
|
||||
tokenClass = "trigger-token field table-row-field";
|
||||
break;
|
||||
default:
|
||||
tokenClass = "trigger-token field";
|
||||
}
|
||||
|
||||
return $(`<span class="${tokenClass}" data-value="${fieldname}" draggable="true" contenteditable="false">${fieldname}<span class="token-remove">×</span></span>`);
|
||||
}
|
||||
|
||||
function initTriggerTokenEvents($token) {
|
||||
// Удаление токена
|
||||
$token.find('.token-remove').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
$(this).parent().remove();
|
||||
});
|
||||
|
||||
// Перетаскивание токена
|
||||
$token.attr('draggable', 'true');
|
||||
|
||||
$token.off('dragstart').on('dragstart', function(e) {
|
||||
window.draggedTriggerElement = this;
|
||||
|
||||
e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify({
|
||||
type: 'trigger-token',
|
||||
value: $(this).data('value'),
|
||||
html: $(this)[0].outerHTML,
|
||||
tokenClass: $(this).attr('class')
|
||||
}));
|
||||
|
||||
$(this).addClass('dragging');
|
||||
});
|
||||
|
||||
$token.off('dragend').on('dragend', function() {
|
||||
$(this).removeClass('dragging');
|
||||
window.draggedTriggerElement = null;
|
||||
});
|
||||
}
|
||||
|
||||
function isPredefinedEvent(trigger) {
|
||||
const predefinedEvents = [
|
||||
"При открытии документа",
|
||||
"when_document_opens",
|
||||
"document_opens"
|
||||
];
|
||||
|
||||
return predefinedEvents.includes(trigger);
|
||||
}
|
||||
|
||||
function openTriggerFieldsEditor(frm, targetFieldname, currentTriggers, fields, grouped_fields) {
|
||||
const $modal = $("#trigger_fields_modal");
|
||||
const $resultContainer = $modal.find(".trigger-result");
|
||||
|
||||
// Очистка предыдущих обработчиков и данных
|
||||
$modal.off('click', '.trigger-field-btn');
|
||||
$modal.off('click', '.select-table-row-trigger-field');
|
||||
$modal.find('.apply-triggers').off('click');
|
||||
$resultContainer.empty();
|
||||
$modal.find(".trigger-field-groups-container").empty();
|
||||
|
||||
// Заполняем текущие триггеры, если они есть
|
||||
if (currentTriggers) {
|
||||
parseAndRenderTriggers($resultContainer, currentTriggers);
|
||||
}
|
||||
|
||||
// Заполняем панель с полями
|
||||
populateTriggerFieldsPanel($modal, frm, grouped_fields);
|
||||
|
||||
// Сохраняем данные о текущем поле и сбрасываем флаг table-row
|
||||
$modal.data("target-field", targetFieldname);
|
||||
$modal.data("is-for-table-row", false);
|
||||
|
||||
// Обработчик для кнопки применения триггеров
|
||||
$modal.find('.apply-triggers').off('click').on('click', function() {
|
||||
const triggers = getTriggerFieldsText($resultContainer);
|
||||
|
||||
// Проверяем, для контейнера строки таблицы или обычного поля
|
||||
if ($modal.data("is-for-table-row")) {
|
||||
const $tableRowContainer = $modal.data("table-row-container");
|
||||
|
||||
// Очищаем контейнер строки таблицы
|
||||
$tableRowContainer.empty();
|
||||
|
||||
// Переносим триггеры из модального окна в контейнер строки таблицы
|
||||
if (triggers) {
|
||||
parseAndRenderTriggers($tableRowContainer, triggers);
|
||||
}
|
||||
} else {
|
||||
// Стандартное поведение для обычного поля
|
||||
const $targetInput = $(frm.wrapper).find(`.trigger-input[data-fieldname="${targetFieldname}_triggers"]`);
|
||||
$targetInput.val(triggers);
|
||||
}
|
||||
|
||||
$modal.modal('hide');
|
||||
});
|
||||
|
||||
// Обработчики для кнопок полей
|
||||
$modal.off('click', '.trigger-field-btn').on('click', '.trigger-field-btn', function() {
|
||||
const fieldname = $(this).data('fieldname');
|
||||
|
||||
// Определяем тип поля по классу кнопки
|
||||
let fieldType = 'field';
|
||||
if ($(this).hasClass('highlight-event')) {
|
||||
fieldType = 'event';
|
||||
} else if ($(this).hasClass('universal-field-btn')) {
|
||||
fieldType = 'universal';
|
||||
} else if ($(this).hasClass('table-field-btn')) {
|
||||
fieldType = 'table';
|
||||
}
|
||||
|
||||
addTokenToTriggers(fieldname, $resultContainer, fieldType);
|
||||
});
|
||||
|
||||
// Обработчик для кнопки выбора полей строк табличных частей
|
||||
$modal.off('click', '.select-table-row-trigger-field').on('click', '.select-table-row-trigger-field', function() {
|
||||
openTableRowFieldSelector(frm, $resultContainer, true);
|
||||
});
|
||||
|
||||
// Показываем модальное окно
|
||||
$modal.modal('show');
|
||||
|
||||
// Инициализируем перетаскивание
|
||||
initTriggerDragAndDrop($modal, $resultContainer);
|
||||
|
||||
// Настраиваем редактируемый контейнер
|
||||
setupTriggerEditableContainer($resultContainer);
|
||||
|
||||
// Фокусируемся на редактируемом поле
|
||||
$resultContainer.focus();
|
||||
}
|
||||
|
||||
function setupTriggerEditableContainer($container) {
|
||||
// Обработчик нажатия клавиш в контейнере триггеров
|
||||
$container.off('keydown').on('keydown', function(e) {
|
||||
// Если нажата клавиша Delete или Backspace
|
||||
if (e.keyCode === 8 || e.keyCode === 46) {
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) {
|
||||
// Текстовый узел - обычное удаление
|
||||
return true;
|
||||
} else {
|
||||
// Возможно, выделен токен
|
||||
const $selectedNode = $(range.commonAncestorContainer);
|
||||
|
||||
if ($selectedNode.hasClass('trigger-token') || $selectedNode.parents('.trigger-token').length) {
|
||||
// Удаляем токен
|
||||
const $token = $selectedNode.hasClass('trigger-token') ?
|
||||
$selectedNode : $selectedNode.parents('.trigger-token');
|
||||
$token.remove();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Обработчик вставки текста
|
||||
$container.off('paste').on('paste', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Получаем вставляемый текст
|
||||
let text = '';
|
||||
if (e.clipboardData || e.originalEvent.clipboardData) {
|
||||
text = (e.clipboardData || e.originalEvent.clipboardData).getData('text/plain');
|
||||
} else if (window.clipboardData) {
|
||||
text = window.clipboardData.getData('Text');
|
||||
}
|
||||
|
||||
// Разбиваем вставляемый текст по запятым и добавляем как токены
|
||||
if (text) {
|
||||
const fields = text.split(',').map(field => field.trim()).filter(field => field);
|
||||
|
||||
fields.forEach(field => {
|
||||
// Определяем тип триггера
|
||||
let fieldType = 'field';
|
||||
|
||||
if (isPredefinedEvent(field)) {
|
||||
fieldType = 'event';
|
||||
} else if (field.startsWith('univ(')) {
|
||||
fieldType = 'universal';
|
||||
} else if (field.includes('_') && !field.includes('_row_')) {
|
||||
// Проверяем, является ли это полем табличной части
|
||||
const parts = field.split('_');
|
||||
if (parts.length >= 2) {
|
||||
fieldType = 'table';
|
||||
}
|
||||
} else if (field.includes('_row_')) {
|
||||
fieldType = 'table-row';
|
||||
}
|
||||
|
||||
// Проверяем, не добавлено ли уже это поле
|
||||
const existingTokens = $container.find(`.trigger-token[data-value="${field}"]`);
|
||||
if (existingTokens.length === 0) {
|
||||
addTokenToTriggers(field, $container, fieldType);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openTriggerFieldsEditorForTableRow(frm, $triggerContainer) {
|
||||
const $modal = $("#trigger_fields_modal");
|
||||
|
||||
// Очистка предыдущих обработчиков и данных
|
||||
$modal.off('click', '.trigger-field-btn');
|
||||
$modal.off('click', '.select-table-row-trigger-field');
|
||||
$modal.find('.apply-triggers').off('click');
|
||||
$modal.find(".trigger-result").empty();
|
||||
$modal.find(".trigger-field-groups-container").empty();
|
||||
|
||||
// Копируем текущие триггеры из контейнера строки таблицы
|
||||
const currentTriggers = getTriggerFieldsText($triggerContainer);
|
||||
|
||||
// Заполняем текущие триггеры в модальное окно триггеров
|
||||
if (currentTriggers) {
|
||||
parseAndRenderTriggers($modal.find(".trigger-result"), currentTriggers);
|
||||
}
|
||||
|
||||
// Заполняем панель с полями
|
||||
populateTriggerFieldsPanel($modal, frm, frm.grouped_trigger_fields || {});
|
||||
|
||||
// Помечаем, что это для контейнера строки таблицы
|
||||
$modal.data("is-for-table-row", true);
|
||||
$modal.data("table-row-container", $triggerContainer);
|
||||
|
||||
// Обработчик для кнопки применения триггеров
|
||||
$modal.find('.apply-triggers').off('click').on('click', function() {
|
||||
const triggers = getTriggerFieldsText($modal.find(".trigger-result"));
|
||||
|
||||
// Проверяем, для контейнера строки таблицы или обычного поля
|
||||
if ($modal.data("is-for-table-row")) {
|
||||
// Очищаем контейнер строки таблицы
|
||||
$triggerContainer.empty();
|
||||
|
||||
// Переносим триггеры из модального окна в контейнер строки таблицы
|
||||
if (triggers) {
|
||||
parseAndRenderTriggers($triggerContainer, triggers);
|
||||
}
|
||||
} else {
|
||||
// Стандартное поведение для обычного поля
|
||||
const targetFieldname = $modal.data("target-field");
|
||||
const $targetInput = $(frm.wrapper).find(`.trigger-input[data-fieldname="${targetFieldname}_triggers"]`);
|
||||
$targetInput.val(triggers);
|
||||
}
|
||||
|
||||
$modal.modal('hide');
|
||||
});
|
||||
|
||||
// Обработчики для кнопок полей
|
||||
$modal.off('click', '.trigger-field-btn').on('click', '.trigger-field-btn', function() {
|
||||
const fieldname = $(this).data('fieldname');
|
||||
|
||||
// Определяем тип поля по классу кнопки
|
||||
let fieldType = 'field';
|
||||
if ($(this).hasClass('highlight-event')) {
|
||||
fieldType = 'event';
|
||||
} else if ($(this).hasClass('universal-field-btn')) {
|
||||
fieldType = 'universal';
|
||||
} else if ($(this).hasClass('table-field-btn')) {
|
||||
fieldType = 'table';
|
||||
}
|
||||
|
||||
addTokenToTriggers(fieldname, $modal.find(".trigger-result"), fieldType);
|
||||
});
|
||||
|
||||
// Обработчик для кнопки выбора поля строки табличной части
|
||||
$modal.off('click', '.select-table-row-trigger-field').on('click', '.select-table-row-trigger-field', function() {
|
||||
openTableRowFieldSelector(frm, $modal.find(".trigger-result"), true);
|
||||
});
|
||||
|
||||
// Увеличиваем z-index модального окна, чтобы оно было поверх других
|
||||
$modal.css('z-index', 1100);
|
||||
|
||||
// Настраиваем фон
|
||||
$('.modal-backdrop').not(':first').css({
|
||||
'z-index': 1050,
|
||||
'opacity': 0.7
|
||||
});
|
||||
|
||||
// Показываем модальное окно
|
||||
$modal.modal('show');
|
||||
|
||||
// Инициализируем перетаскивание
|
||||
initTriggerDragAndDrop($modal, $modal.find(".trigger-result"));
|
||||
|
||||
// Настраиваем редактируемый контейнер
|
||||
setupTriggerEditableContainer($modal.find(".trigger-result"));
|
||||
|
||||
// Фокусируемся на редактируемом поле
|
||||
$modal.find(".trigger-result").focus();
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,612 @@
|
|||
function addTokenStyles() {
|
||||
if ($("#token-styles").length) return;
|
||||
|
||||
const tokenStyles = `
|
||||
<style id="token-styles">
|
||||
/* Стили для токенов */
|
||||
.formula-token, .trigger-token {
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
margin: 2px;
|
||||
border-radius: 3px;
|
||||
cursor: move;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.formula-token .token-remove,
|
||||
.trigger-token .token-remove {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #ff5555;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Стили для полей из строк табличных частей */
|
||||
.table-field-btn,
|
||||
.trigger-token.table-field,
|
||||
.formula-token.table-field {
|
||||
background-color: #e8f4fd !important;
|
||||
border-color: #bee5eb !important;
|
||||
}
|
||||
|
||||
.table-field-btn:hover {
|
||||
background-color: #d1ecf1 !important;
|
||||
}
|
||||
|
||||
.btn.select-table-field {
|
||||
background-color: #e8f4fd;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.btn.select-table-field:hover {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #a6d9e8;
|
||||
}
|
||||
|
||||
|
||||
.formula-token:hover .token-remove,
|
||||
.trigger-token:hover .token-remove {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Стили для обычных полей */
|
||||
.formula-token.field,
|
||||
.trigger-token.field {
|
||||
background-color: #e3efff;
|
||||
border: 1px solid #abc9f5;
|
||||
}
|
||||
|
||||
/* Стили для операторов */
|
||||
.formula-token.operator {
|
||||
background-color: #ffe8e3;
|
||||
border: 1px solid #f5c3b9;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Стили для универсальных полей */
|
||||
.universal-field-btn,
|
||||
.trigger-token.universal-field,
|
||||
.formula-token.universal-field {
|
||||
background-color: #eff8e6 !important;
|
||||
border-color: #b7d69d !important;
|
||||
}
|
||||
|
||||
/* Стили для полей отчетов */
|
||||
.report-field-btn,
|
||||
.trigger-token.report-field,
|
||||
.formula-token.report-field {
|
||||
background-color: #f5e6ff !important;
|
||||
border-color: #d9b3ff !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.report-field-btn:hover {
|
||||
background-color: #ebccff !important;
|
||||
}
|
||||
|
||||
/* Стили для полей строк */
|
||||
.row-field-btn {
|
||||
background-color: #fff3cd !important;
|
||||
border-color: #ffeeba !important;
|
||||
}
|
||||
|
||||
/* Стили для контейнеров */
|
||||
.formula-result,
|
||||
.trigger-result {
|
||||
min-height: 30px;
|
||||
padding: 5px;
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Стили для фильтров отчетов */
|
||||
.filters-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
</style>`;
|
||||
|
||||
$('head').append(tokenStyles);
|
||||
}
|
||||
|
||||
function createFormulaFieldHtml(field, dataAttr) {
|
||||
return $('<div class="formula-row" style="display:flex;justify-content:space-between;gap:20px;align-items:center;width:100%;margin-bottom:10px;">' +
|
||||
'<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="' + field.fieldname + '" ' + dataAttr + ' style="flex:1;">' +
|
||||
'<div class="form-group horizontal">' +
|
||||
'<div class="clearfix"><label class="control-label">' + field.label + ' (' + field.fieldname + ')</label></div>' +
|
||||
'<div class="control-input-wrapper" style="display:flex;align-items:center;">' +
|
||||
'<div class="control-input" style="flex:1;">' +
|
||||
'<input type="text" class="input-with-feedback form-control formula-input" data-fieldtype="Data" data-fieldname="' + field.fieldname + '" ' + dataAttr + ' autocomplete="off" placeholder="Введите формулу">' +
|
||||
'</div>' +
|
||||
'<button class="btn btn-sm btn-primary open-formula-builder" data-fieldname="' + field.fieldname + '" data-is-table="' + (dataAttr ? 'true' : 'false') + '" style="margin-left:8px;"><i class="fa fa-plus"></i></button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="frappe-control input-max-width" data-fieldtype="Data" data-fieldname="' + field.fieldname + '_triggers" ' + dataAttr + ' style="flex:1;">' +
|
||||
'<div class="form-group horizontal">' +
|
||||
'<div class="clearfix"><label class="control-label">Обновлять при изменении</label></div>' +
|
||||
'<div class="control-input-wrapper" style="display:flex;align-items:center;">' +
|
||||
'<div class="control-input" style="flex:1;">' +
|
||||
'<input type="text" class="input-with-feedback form-control trigger-input" data-fieldtype="Data" data-fieldname="' + field.fieldname + '_triggers" ' + dataAttr + ' autocomplete="off" placeholder="Введите поля через запятую">' +
|
||||
'</div>' +
|
||||
'<button class="btn btn-sm btn-primary open-trigger-editor" data-fieldname="' + field.fieldname + '" style="margin-left:8px;"><i class="fa fa-plus"></i></button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>');
|
||||
}
|
||||
|
||||
function attachFormulaBuilderEvents(frm, fields, grouped_fields) {
|
||||
// Сделаем поля ввода формул редактируемыми
|
||||
$(frm.wrapper).find('.formula-input').prop('readonly', false);
|
||||
|
||||
// Сначала удаляем существующие обработчики событий
|
||||
$(frm.wrapper).off('click', '.open-formula-builder');
|
||||
|
||||
// Обработчик для кнопок открытия визуального редактора
|
||||
$(frm.wrapper).on('click', '.open-formula-builder', function() {
|
||||
const fieldname = $(this).data('fieldname');
|
||||
const isTableField = $(this).data('is-table') === true;
|
||||
const currentFormula = $(frm.wrapper).find(`.formula-input[data-fieldname="${fieldname}"]`).val();
|
||||
|
||||
openFormulaBuilder(frm, fieldname, isTableField, currentFormula, fields, grouped_fields);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function attachTriggerEditorEvents(frm, fields, grouped_fields) {
|
||||
// Сначала удаляем существующие обработчики событий
|
||||
$(frm.wrapper).off('click', '.open-trigger-editor');
|
||||
|
||||
// Обработчик для кнопок открытия редактора полей изменения
|
||||
$(frm.wrapper).on('click', '.open-trigger-editor', function() {
|
||||
const fieldname = $(this).data('fieldname');
|
||||
const currentTriggers = $(frm.wrapper).find(`.trigger-input[data-fieldname="${fieldname}_triggers"]`).val();
|
||||
|
||||
openTriggerFieldsEditor(frm, fieldname, currentTriggers, fields, grouped_fields);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function addUniversalFieldsSection(wrapper) {
|
||||
const universalSection = $('<div class="universal-fields-section" style="width:100%;margin-top:20px;padding:10px;border:1px solid #ddd;border-radius:5px;"></div>');
|
||||
universalSection.append('<h4 style="margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;">Универсальные поля <button class="btn btn-sm btn-primary add-universal-field"><i class="fa fa-plus"></i> Добавить поле</button></h4>');
|
||||
universalSection.append('<div class="universal-fields-list" style="display:flex;flex-wrap:wrap;gap:10px;"></div>');
|
||||
wrapper.append(universalSection);
|
||||
}
|
||||
|
||||
function addReportFieldsSection(wrapper) {
|
||||
const reportSection = $('<div class="report-fields-section" style="width:100%;margin-top:20px;padding:10px;border:1px solid #ddd;border-radius:5px;"></div>');
|
||||
reportSection.append('<h4 style="margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;">Поля отчетов <button class="btn btn-sm btn-primary add-report-field"><i class="fa fa-plus"></i> Добавить поле отчета</button></h4>');
|
||||
reportSection.append('<div class="report-fields-list" style="display:flex;flex-wrap:wrap;gap:10px;"></div>');
|
||||
wrapper.append(reportSection);
|
||||
}
|
||||
|
||||
function addTableRowFormulasSection(wrapper) {
|
||||
const tableRowSection = $('<div class="table-row-formulas-section" style="width:100%;margin-top:20px;padding:10px;border:1px solid #ddd;border-radius:5px;"></div>');
|
||||
tableRowSection.append('<h4 style="margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;">Формулы для строк т.ч. <button class="btn btn-sm btn-primary add-table-row-formula"><i class="fa fa-plus"></i> Добавить формулу</button></h4>');
|
||||
tableRowSection.append('<div class="table-row-formulas-list" style="width:100%;"></div>');
|
||||
wrapper.append(tableRowSection);
|
||||
}
|
||||
|
||||
function populateFieldsPanel($modal, frm, grouped_fields, isTableField) {
|
||||
const $container = $modal.find(".field-groups-container");
|
||||
$container.empty();
|
||||
|
||||
// Добавляем секцию предопределенных событий
|
||||
const $predefinedEventsGroup = $(`
|
||||
<div class="field-group">
|
||||
<div class="field-group-title">Предопределенные события</div>
|
||||
<div class="field-buttons-container" data-group="predefined-events"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const $predefinedButtonsContainer = $predefinedEventsGroup.find('.field-buttons-container');
|
||||
|
||||
// Добавляем кнопки для предопределенных событий
|
||||
const predefinedEvents = [
|
||||
{ name: "При открытии документа", id: "При открытии документа" }
|
||||
];
|
||||
|
||||
predefinedEvents.forEach(event => {
|
||||
const $eventBtn = $(`
|
||||
<button class="btn btn-sm field-btn highlight-event draggable-item"
|
||||
data-fieldname="${event.id}"
|
||||
data-label="${event.name}"
|
||||
data-group="predefined-events"
|
||||
data-field-type="event"
|
||||
draggable="true">
|
||||
${event.name}
|
||||
</button>
|
||||
`);
|
||||
|
||||
$predefinedButtonsContainer.append($eventBtn);
|
||||
});
|
||||
|
||||
// Добавляем группу предопределенных событий в начало контейнера
|
||||
$container.prepend($predefinedEventsGroup);
|
||||
|
||||
// Добавляем стандартные поля
|
||||
Object.keys(grouped_fields).forEach(group => {
|
||||
const $fieldGroup = $(`
|
||||
<div class="field-group">
|
||||
<div class="field-group-title">${group}</div>
|
||||
<div class="field-buttons-container" data-group="${group}"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const $buttonsContainer = $fieldGroup.find('.field-buttons-container');
|
||||
|
||||
grouped_fields[group].forEach(field => {
|
||||
// Определяем, является ли это поле таблицы или нет
|
||||
const isTableGroupField = group !== "Основные поля";
|
||||
|
||||
// Создаем кнопку поля с правильными атрибутами
|
||||
const btnClass = isTableGroupField ? "field-btn table-field-btn" : "field-btn";
|
||||
const $fieldBtn = $(`
|
||||
<button class="btn btn-sm ${btnClass} draggable-item"
|
||||
data-fieldname="${field.fieldname}"
|
||||
data-label="${field.label}"
|
||||
data-group="${group}"
|
||||
data-field-type="${isTableGroupField ? 'table' : 'field'}"
|
||||
draggable="true">
|
||||
${field.label}
|
||||
</button>
|
||||
`);
|
||||
|
||||
$buttonsContainer.append($fieldBtn);
|
||||
});
|
||||
|
||||
$container.append($fieldGroup);
|
||||
});
|
||||
|
||||
// Добавляем секцию универсальных полей
|
||||
if (frm.doc.universal_fields_storage) {
|
||||
const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}");
|
||||
|
||||
if (Object.keys(universalFields).length > 0) {
|
||||
const $universalFieldGroup = $(`
|
||||
<div class="field-group">
|
||||
<div class="field-group-title">Универсальные поля</div>
|
||||
<div class="field-buttons-container" data-group="universal-fields"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const $universalButtonsContainer = $universalFieldGroup.find('.field-buttons-container');
|
||||
|
||||
Object.keys(universalFields).forEach(fieldId => {
|
||||
const field = universalFields[fieldId];
|
||||
|
||||
if (!field || !field.name) return;
|
||||
|
||||
const $fieldBtn = $(`
|
||||
<button class="btn btn-sm universal-field-btn draggable-item"
|
||||
data-field-id="${fieldId}"
|
||||
data-fieldname="univ(${field.name})"
|
||||
data-label="${field.name}"
|
||||
data-group="universal-fields"
|
||||
data-field-type="universal"
|
||||
draggable="true">
|
||||
${field.name}
|
||||
</button>
|
||||
`);
|
||||
|
||||
$universalButtonsContainer.append($fieldBtn);
|
||||
});
|
||||
|
||||
$container.append($universalFieldGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем секцию полей отчетов
|
||||
if (frm.doc.report_fields_storage) {
|
||||
const reportFields = JSON.parse(frm.doc.report_fields_storage || "{}");
|
||||
|
||||
if (Object.keys(reportFields).length > 0) {
|
||||
const $reportFieldGroup = $(`
|
||||
<div class="field-group">
|
||||
<div class="field-group-title">Поля отчетов</div>
|
||||
<div class="field-buttons-container" data-group="report-fields"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const $reportButtonsContainer = $reportFieldGroup.find('.field-buttons-container');
|
||||
|
||||
Object.keys(reportFields).forEach(fieldId => {
|
||||
const field = reportFields[fieldId];
|
||||
|
||||
if (!field || !field.name) return;
|
||||
|
||||
const $fieldBtn = $(`
|
||||
<button class="btn btn-sm report-field-btn draggable-item"
|
||||
data-field-id="${fieldId}"
|
||||
data-fieldname="report(${field.name})"
|
||||
data-label="${field.name}"
|
||||
data-group="report-fields"
|
||||
data-field-type="report"
|
||||
draggable="true">
|
||||
report(${field.name})
|
||||
</button>
|
||||
`);
|
||||
|
||||
$reportButtonsContainer.append($fieldBtn);
|
||||
});
|
||||
|
||||
$container.append($reportFieldGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем группу для полей строк табличных частей
|
||||
const $tableRowGroup = $(`
|
||||
<div class="field-group">
|
||||
<div class="field-group-title">Поля строк табличных частей</div>
|
||||
<div class="field-buttons-container table-row-fields-container">
|
||||
<button class="btn btn-warning btn-sm select-table-row-field">
|
||||
<i class="fa fa-plus"></i> Выбрать поле строки т.ч.
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$container.append($tableRowGroup);
|
||||
|
||||
// НОВОЕ: Добавляем группу для полей из конкретных строк табличных частей
|
||||
const $tableFieldGroup = $(`
|
||||
<div class="field-group">
|
||||
<div class="field-group-title">Поля из строк табличных частей</div>
|
||||
<div class="field-buttons-container table-field-container">
|
||||
<button class="btn btn-info btn-sm select-table-field">
|
||||
<i class="fa fa-table"></i> Поле из строки т.ч.
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$container.append($tableFieldGroup);
|
||||
|
||||
// Добавляем обработчик для кнопки выбора поля строки табличной части
|
||||
$tableRowGroup.find('.select-table-row-field').on('click', function() {
|
||||
// Определяем целевой контейнер в зависимости от контекста
|
||||
const targetContainer = $modal.hasClass('trigger-focus') ?
|
||||
$modal.find(".trigger-result") :
|
||||
$modal.find(".formula-result");
|
||||
|
||||
openTableRowFieldSelector(frm, targetContainer, $modal.hasClass('trigger-focus'));
|
||||
});
|
||||
|
||||
// НОВОЕ: Добавляем обработчик для кнопки выбора поля из строки табличной части
|
||||
$tableFieldGroup.find('.select-table-field').on('click', function() {
|
||||
const targetContainer = $modal.find(".formula-result");
|
||||
openTableFieldSelector(frm, targetContainer, false);
|
||||
});
|
||||
}
|
||||
|
||||
function populateTriggerFieldsPanel($modal, frm, grouped_fields) {
|
||||
const $container = $modal.find(".trigger-field-groups-container");
|
||||
$container.empty();
|
||||
|
||||
// Добавляем секцию предопределенных событий
|
||||
const $predefinedEventsGroup = $(`
|
||||
<div class="trigger-field-group">
|
||||
<div class="trigger-field-group-title">Предопределенные события</div>
|
||||
<div class="trigger-field-buttons-container" data-group="predefined-events"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const $predefinedButtonsContainer = $predefinedEventsGroup.find('.trigger-field-buttons-container');
|
||||
|
||||
// Добавляем кнопки для предопределенных событий
|
||||
const predefinedEvents = [
|
||||
{ name: "При открытии документа", id: "При открытии документа" }
|
||||
];
|
||||
|
||||
predefinedEvents.forEach(event => {
|
||||
const $eventBtn = $(`
|
||||
<button class="btn btn-sm trigger-field-btn draggable-item highlight-event"
|
||||
data-fieldname="${event.id}"
|
||||
data-label="${event.name}"
|
||||
data-group="predefined-events"
|
||||
draggable="true">
|
||||
${event.name}
|
||||
</button>
|
||||
`);
|
||||
|
||||
$predefinedButtonsContainer.append($eventBtn);
|
||||
});
|
||||
|
||||
// Добавляем группу предопределенных событий в начало контейнера
|
||||
$container.prepend($predefinedEventsGroup);
|
||||
|
||||
// Добавляем секцию универсальных полей, если они есть
|
||||
if (frm.doc.universal_fields_storage) {
|
||||
const universalFields = JSON.parse(frm.doc.universal_fields_storage || "{}");
|
||||
|
||||
if (Object.keys(universalFields).length > 0) {
|
||||
const $universalFieldGroup = $(`
|
||||
<div class="trigger-field-group">
|
||||
<div class="trigger-field-group-title">Универсальные поля</div>
|
||||
<div class="trigger-field-buttons-container" data-group="universal-fields"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const $universalButtonsContainer = $universalFieldGroup.find('.trigger-field-buttons-container');
|
||||
|
||||
Object.keys(universalFields).forEach(fieldId => {
|
||||
const field = universalFields[fieldId];
|
||||
|
||||
if (!field || !field.name) return;
|
||||
|
||||
const $fieldBtn = $(`
|
||||
<button class="btn btn-sm trigger-field-btn universal-field-btn draggable-item"
|
||||
data-fieldname="univ(${field.name})"
|
||||
data-label="${field.name}"
|
||||
data-group="universal-fields"
|
||||
data-field-id="${fieldId}"
|
||||
draggable="true">
|
||||
${field.name}
|
||||
</button>
|
||||
`);
|
||||
|
||||
$universalButtonsContainer.append($fieldBtn);
|
||||
});
|
||||
|
||||
$container.append($universalFieldGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем обычные поля
|
||||
Object.keys(grouped_fields).forEach(group => {
|
||||
// Пропускаем пустые группы
|
||||
if (!grouped_fields[group] || grouped_fields[group].length === 0) return;
|
||||
|
||||
const $fieldGroup = $(`
|
||||
<div class="trigger-field-group">
|
||||
<div class="trigger-field-group-title">${group}</div>
|
||||
<div class="trigger-field-buttons-container" data-group="${group}"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const $buttonsContainer = $fieldGroup.find('.trigger-field-buttons-container');
|
||||
|
||||
grouped_fields[group].forEach(field => {
|
||||
// Создаем кнопку поля с информацией о типе поля
|
||||
const isTableField = group !== "Основные поля";
|
||||
const btnClass = isTableField ? "trigger-field-btn table-field-btn" : "trigger-field-btn";
|
||||
|
||||
const $fieldBtn = $(`
|
||||
<button class="btn btn-sm ${btnClass} draggable-item"
|
||||
data-fieldname="${field.fieldname}"
|
||||
data-label="${field.label}"
|
||||
data-group="${group}"
|
||||
data-fieldtype="${field.fieldtype || ''}"
|
||||
draggable="true">
|
||||
${field.label}
|
||||
</button>
|
||||
`);
|
||||
|
||||
$buttonsContainer.append($fieldBtn);
|
||||
});
|
||||
|
||||
$container.append($fieldGroup);
|
||||
});
|
||||
|
||||
// Добавляем группу для полей строк табличных частей
|
||||
const $tableRowGroup = $(`
|
||||
<div class="trigger-field-group">
|
||||
<div class="trigger-field-group-title">Поля строк табличных частей</div>
|
||||
<div class="trigger-field-buttons-container table-row-fields-container">
|
||||
<button class="btn btn-warning btn-sm select-table-row-trigger-field">
|
||||
<i class="fa fa-plus"></i> Выбрать поле строки т.ч.
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$container.append($tableRowGroup);
|
||||
|
||||
// Добавляем стили для выделения различных типов триггеров
|
||||
if (!$("#trigger-field-styles").length) {
|
||||
$("head").append(`
|
||||
<style id="trigger-field-styles">
|
||||
.highlight-event {
|
||||
background-color: #f8f1c6 !important;
|
||||
border-color: #e6d292 !important;
|
||||
}
|
||||
.highlight-event:hover {
|
||||
background-color: #f0e6b1 !important;
|
||||
}
|
||||
.trigger-token.highlight-event {
|
||||
background-color: #f8f1c6;
|
||||
border-color: #e6d292;
|
||||
}
|
||||
|
||||
.universal-field-btn {
|
||||
background-color: #eff8e6 !important;
|
||||
border-color: #b7d69d !important;
|
||||
}
|
||||
.universal-field-btn:hover {
|
||||
background-color: #deedcf !important;
|
||||
}
|
||||
.trigger-token.universal-field {
|
||||
background-color: #eff8e6;
|
||||
border-color: #b7d69d;
|
||||
}
|
||||
|
||||
.table-field-btn {
|
||||
background-color: #e3efff !important;
|
||||
border-color: #abc9f5 !important;
|
||||
}
|
||||
.table-field-btn:hover {
|
||||
background-color: #d0e5ff !important;
|
||||
}
|
||||
.trigger-token.table-field {
|
||||
background-color: #e3efff;
|
||||
border-color: #abc9f5;
|
||||
}
|
||||
|
||||
.table-row-field-btn {
|
||||
background-color: #fff3cd !important;
|
||||
border-color: #ffeeba !important;
|
||||
}
|
||||
.table-row-field-btn:hover {
|
||||
background-color: #ffeeba !important;
|
||||
}
|
||||
.trigger-token.table-row-field {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeeba;
|
||||
}
|
||||
|
||||
.btn.select-table-row-field,
|
||||
.btn.select-table-row-trigger-field {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeeba;
|
||||
}
|
||||
|
||||
.btn.select-table-row-field:hover,
|
||||
.btn.select-table-row-trigger-field:hover {
|
||||
background-color: #ffeeba;
|
||||
border-color: #ffdf7e;
|
||||
}
|
||||
</style>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function setEndOfContentEditable(contentEditableElement) {
|
||||
let range, selection;
|
||||
range = document.createRange();
|
||||
range.selectNodeContents(contentEditableElement);
|
||||
range.collapse(false);
|
||||
selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,171 @@
|
|||
function getTableDoctypes(target_doctype, table_doctypes) {
|
||||
// Пытаемся получить информацию о таблицах через API
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.client.get",
|
||||
args: {
|
||||
doctype: "DocType",
|
||||
name: target_doctype
|
||||
},
|
||||
async: false,
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.fields) {
|
||||
r.message.fields.forEach(field => {
|
||||
if (field.fieldtype === 'Table' && field.options) {
|
||||
table_doctypes[field.fieldname] = field.options;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Если данные не получены, используем альтернативный способ через meta
|
||||
if (Object.keys(table_doctypes).length === 0) {
|
||||
|
||||
const meta = frappe.get_meta(target_doctype);
|
||||
if (meta && meta.fields) {
|
||||
meta.fields.forEach(field => {
|
||||
if (field.fieldtype === 'Table' && field.options) {
|
||||
table_doctypes[field.fieldname] = field.options;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем результат
|
||||
if (Object.keys(table_doctypes).length === 0) {
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// Словарь для сопоставления названий месяцев с их номерами
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
/home/frappe/frappe-bench/apps/formula_editor/node_modules
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"eslint": "^9.29.0"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue