diff --git a/formula_editor/public/css/formula_editor.css b/formula_editor/public/css/formula_editor.css
deleted file mode 100644
index e3093a0..0000000
--- a/formula_editor/public/css/formula_editor.css
+++ /dev/null
@@ -1,359 +0,0 @@
-/**
- * Formula Editor Styles
- */
-
- .formula-editor-container {
- position: relative;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- min-height: 100px;
- display: flex;
- flex-direction: column;
- }
-
- .formula-editor-container.has-error {
- border-color: var(--red-500);
- }
-
- .formula-editor-wrapper {
- display: flex;
- flex-direction: column;
- height: 100%;
- }
-
- .formula-editor-top {
- border-bottom: 1px solid var(--border-color);
- padding: 4px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- .formula-editor-main {
- display: flex;
- flex: 1;
- min-height: 200px;
- }
-
- .formula-editor-section {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
-
- .formula-editor-code-section {
- flex: 2;
- border-right: 1px solid var(--border-color);
- }
-
- .formula-editor-panels-section {
- flex: 1;
- min-width: 250px;
- max-width: 300px;
- }
-
- .formula-editor-bottom {
- border-top: 1px solid var(--border-color);
- padding: 4px 8px;
- display: flex;
- align-items: center;
- }
-
- .formula-editor-toolbar {
- display: flex;
- align-items: center;
- gap: 4px;
- }
-
- .formula-editor-code-container {
- flex: 1;
- position: relative;
- overflow: hidden;
- }
-
- .formula-editor-code {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- font-family: monospace;
- font-size: 13px;
- height: 100%;
- width: 100%;
- }
-
- .formula-editor-textarea {
- width: 100%;
- height: 100%;
- resize: none;
- border: none;
- padding: 8px;
- font-family: monospace;
- font-size: 13px;
- }
-
- .formula-editor-panel {
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: hidden;
- }
-
- .panel-header {
- padding: 8px;
- font-weight: bold;
- border-bottom: 1px solid var(--border-color);
- }
-
- .panel-search {
- padding: 4px 8px;
- border-bottom: 1px solid var(--border-color);
- }
-
- .panel-content {
- padding: 0;
- overflow-y: auto;
- flex: 1;
- }
-
- .field-group {
- margin-bottom: 2px;
- }
-
- .group-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 6px 8px;
- cursor: pointer;
- background-color: var(--bg-light-gray);
- }
-
- .group-header:hover {
- background-color: var(--bg-gray);
- }
-
- .group-name {
- font-weight: 500;
- }
-
- .group-count {
- font-size: 0.8em;
- color: var(--text-muted);
- margin-right: 4px;
- }
-
- .group-content {
- padding: 4px 0;
- }
-
- .collapsed .group-content {
- display: none;
- }
-
- .field-item {
- display: flex;
- align-items: center;
- padding: 4px 8px 4px 16px;
- cursor: pointer;
- border-left: 2px solid transparent;
- }
-
- .field-item:hover {
- background-color: var(--bg-light-gray);
- border-left-color: var(--primary);
- }
-
- .field-icon {
- margin-right: 8px;
- color: var(--text-muted);
- width: 16px;
- text-align: center;
- }
-
- .field-label {
- flex: 1;
- margin-right: 4px;
- }
-
- .field-type {
- font-size: 0.8em;
- color: var(--text-muted);
- }
-
- .category-item {
- padding: 6px 12px;
- cursor: pointer;
- border-left: 2px solid transparent;
- }
-
- .category-item:hover,
- .category-item.selected {
- background-color: var(--bg-light-gray);
- }
-
- .category-item.selected {
- border-left-color: var(--primary);
- font-weight: 500;
- }
-
- .function-item {
- padding: 8px;
- border-bottom: 1px solid var(--border-color);
- cursor: pointer;
- }
-
- .function-item:hover {
- background-color: var(--bg-light-gray);
- }
-
- .function-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 4px;
- }
-
- .function-name {
- font-weight: 500;
- margin-right: 8px;
- }
-
- .function-syntax {
- font-family: monospace;
- font-size: 0.9em;
- color: var(--text-muted);
- }
-
- .function-description {
- font-size: 0.9em;
- color: var(--text-muted);
- }
-
- .formula-editor-error-panel {
- color: var(--red-500);
- display: flex;
- align-items: center;
- width: 100%;
- }
-
- .formula-editor-error-panel i {
- margin-right: 8px;
- }
-
- .formula-editor-error-panel.hidden {
- display: none;
- }
-
- .formula-editor-btn {
- margin-left: 4px;
- }
-
- .formula-list-container {
- padding: 8px;
- }
-
- .formula-list-item {
- border: 1px solid var(--border-color);
- border-radius: 4px;
- margin-bottom: 8px;
- padding: 8px;
- }
-
- .formula-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px;
- }
-
- .formula-name {
- font-weight: 500;
- margin-right: 8px;
- }
-
- .formula-field {
- font-size: 0.9em;
- color: var(--text-muted);
- }
-
- .formula-actions {
- display: flex;
- gap: 4px;
- }
-
- .formula-code {
- padding: 8px;
- background-color: var(--bg-light-gray);
- border-radius: 4px;
- font-family: monospace;
- font-size: 0.9em;
- margin-bottom: 8px;
- white-space: pre-wrap;
- }
-
- .formula-description {
- font-size: 0.9em;
- color: var(--text-muted);
- }
-
- .formula-toggle-btn.disabled {
- opacity: 0.5;
- }
-
- .formula-calculated-field {
- background-color: rgba(var(--primary-rgb), 0.1);
- position: relative;
- }
-
- .formula-calculated-field::after {
- content: "ƒ";
- position: absolute;
- top: 0;
- right: 4px;
- font-size: 12px;
- color: var(--primary);
- font-weight: bold;
- }
-
- .empty-state {
- padding: 16px;
- text-align: center;
- color: var(--text-muted);
- }
-
- .formula-editor-container.read-only .formula-editor-code,
- .formula-editor-container.read-only .formula-editor-textarea {
- background-color: var(--bg-light-gray);
- cursor: not-allowed;
- }
-
- .formula-editor-report-btn {
- margin-left: 8px;
- }
-
- .category-badge {
- background-color: var(--bg-light-gray);
- font-size: 0.8em;
- padding: 2px 4px;
- border-radius: 4px;
- margin-left: auto;
- }
-
- /* For trigger mode */
- .formula-editor-container.trigger-mode .formula-editor-panels-section {
- flex: 1.5;
- max-width: 400px;
- }
-
- /* For report mode */
- .formula-editor-container.report-mode .formula-editor-code-section {
- flex: 1.5;
- }
-
- /* Hidden elements */
- .hidden {
- display: none !important;
- }
\ No newline at end of file
diff --git a/formula_editor/public/js/core.js b/formula_editor/public/js/core.js
deleted file mode 100644
index 244992a..0000000
--- a/formula_editor/public/js/core.js
+++ /dev/null
@@ -1,466 +0,0 @@
-/**
- * Formula Editor Core
- * Contains the main FormulaEditor class and core functionality
- */
-
-import {
- safeEval,
- validateFormula,
- extractReferencedFields,
- generateUniqueId,
- debounce,
- formatErrorMessage,
- showToast,
- isEmpty,
- deepClone,
- safeParseJSON
-} from './utils';
-
-class FormulaEditor {
- /**
- * Create a new FormulaEditor instance
- * @param {Object} options - Configuration options
- * @param {string} options.parent - Parent element selector or element
- * @param {string} options.doctype - DocType name
- * @param {string} [options.fieldname] - Field name
- * @param {Object} [options.doc] - Current document
- * @param {Function} [options.on_change] - Change callback
- * @param {Array} [options.custom_fields] - Additional custom fields
- * @param {boolean} [options.is_report] - Whether this is used in a report
- * @param {boolean} [options.is_trigger] - Whether this is used for triggers
- */
- constructor(options) {
- this.options = Object.assign({
- parent: null,
- doctype: null,
- fieldname: null,
- doc: null,
- on_change: null,
- custom_fields: [],
- is_report: false,
- is_trigger: false
- }, options);
-
- // Validate required options
- if (!this.options.parent) {
- throw new Error(__('Parent element is required for Formula Editor'));
- }
-
- if (!this.options.doctype) {
- throw new Error(__('DocType is required for Formula Editor'));
- }
-
- // Initialize properties
- this.initialized = false;
- this.formula = '';
- this.fields = [];
- this.errors = [];
- this.meta = null;
- this.parent = typeof this.options.parent === 'string'
- ? document.querySelector(this.options.parent)
- : this.options.parent;
- this.editor_id = generateUniqueId('formula-editor');
-
- // Create debounced functions
- this.debouncedValidate = debounce(this.validate.bind(this), 500);
- this.debouncedOnChange = debounce(this.handleChange.bind(this), 300);
-
- // Internal data storage
- this._cached_fields = null;
- this._field_options = null;
- this._referenced_fields = [];
-
- // Initialize the editor
- this.init();
- }
-
- /**
- * Initialize the editor
- * @private
- */
- async init() {
- try {
- if (this.initialized) return;
-
- // Fetch metadata for the DocType
- await this.fetchMeta();
-
- // Get available fields
- await this.getFields();
-
- // Create the UI elements
- this.createElements();
-
- // Bind events
- this.bindEvents();
-
- this.initialized = true;
-
- // Initialize with current value if available
- if (this.options.doc && this.options.fieldname) {
- this.setValue(this.options.doc[this.options.fieldname] || '');
- }
-
- } catch (error) {
- console.error('Error initializing Formula Editor:', error);
- this.showError(__('Failed to initialize Formula Editor'));
- }
- }
-
- /**
- * Fetch DocType metadata
- * @private
- */
- async fetchMeta() {
- try {
- this.meta = frappe.get_meta(this.options.doctype);
-
- if (!this.meta) {
- // If meta is not in cache, fetch it
- this.meta = await frappe.model.with_doctype(this.options.doctype);
- }
- } catch (error) {
- console.error('Error fetching metadata:', error);
- throw new Error(__('Could not fetch metadata for {0}', [this.options.doctype]));
- }
- }
-
- /**
- * Get available fields for the formula
- * @private
- */
- async getFields() {
- // Return cached fields if available
- if (this._cached_fields) {
- this.fields = deepClone(this._cached_fields);
- return;
- }
-
- try {
- const fields = [];
-
- // Add standard fields
- this.addStandardFields(fields);
-
- // Add fields from the DocType
- this.addDocTypeFields(fields);
-
- // Add custom fields if provided
- if (Array.isArray(this.options.custom_fields)) {
- fields.push(...this.options.custom_fields);
- }
-
- // Save and cache the fields
- this.fields = fields;
- this._cached_fields = deepClone(fields);
-
- } catch (error) {
- console.error('Error getting fields:', error);
- throw new Error(__('Could not get fields for Formula Editor'));
- }
- }
-
- /**
- * Add standard fields like 'doc', etc.
- * @param {Array} fields - Fields array to add to
- * @private
- */
- addStandardFields(fields) {
- // Add 'doc' as a standard field
- fields.push({
- fieldname: 'doc',
- label: __('Current Document'),
- type: 'Object',
- standard: true
- });
-
- // Add other standard fields based on context
- if (this.options.is_report) {
- fields.push({
- fieldname: 'row',
- label: __('Current Row'),
- type: 'Object',
- standard: true
- });
- }
- }
-
- /**
- * Add fields from the DocType
- * @param {Array} fields - Fields array to add to
- * @private
- */
- addDocTypeFields(fields) {
- if (!this.meta || !this.meta.fields) return;
-
- // Add all fields from the DocType
- for (const field of this.meta.fields) {
- // Skip certain field types
- if (['Section Break', 'Column Break', 'Tab Break', 'HTML', 'Button'].includes(field.fieldtype)) {
- continue;
- }
-
- fields.push({
- fieldname: field.fieldname,
- label: field.label || field.fieldname,
- type: field.fieldtype,
- options: field.options,
- table_fields: this.getTableFields(field),
- is_child_table: field.fieldtype === 'Table'
- });
- }
- }
-
- /**
- * Get fields for a Table type field
- * @param {Object} field - The table field
- * @returns {Array} Child table fields
- * @private
- */
- getTableFields(field) {
- if (field.fieldtype !== 'Table' || !field.options) return [];
-
- const childFields = [];
- const childMeta = frappe.get_meta(field.options);
-
- if (!childMeta || !childMeta.fields) return [];
-
- for (const childField of childMeta.fields) {
- // Skip certain field types
- if (['Section Break', 'Column Break', 'Tab Break', 'HTML', 'Button'].includes(childField.fieldtype)) {
- continue;
- }
-
- childFields.push({
- fieldname: childField.fieldname,
- label: childField.label || childField.fieldname,
- type: childField.fieldtype,
- options: childField.options
- });
- }
-
- return childFields;
- }
-
- /**
- * Create UI elements for the editor
- * @private
- */
- createElements() {
- // This is a placeholder - UI creation will be moved to ui.js
- // Here we just create the basic container
- this.container = document.createElement('div');
- this.container.id = this.editor_id;
- this.container.className = 'formula-editor-container';
-
- // Create a simple textarea for now (will be replaced by proper UI)
- this.textarea = document.createElement('textarea');
- this.textarea.className = 'formula-editor-input';
- this.textarea.placeholder = __('Enter your formula here...');
-
- // Error display element
- this.errorEl = document.createElement('div');
- this.errorEl.className = 'formula-editor-error hidden';
-
- // Append to container
- this.container.appendChild(this.textarea);
- this.container.appendChild(this.errorEl);
-
- // Append to parent
- this.parent.appendChild(this.container);
- }
-
- /**
- * Bind events to the editor elements
- * @private
- */
- bindEvents() {
- if (!this.textarea) return;
-
- // Input event for formula changes
- this.textarea.addEventListener('input', () => {
- this.formula = this.textarea.value;
- this.debouncedValidate();
- this.debouncedOnChange();
- });
-
- // Focus/blur events
- this.textarea.addEventListener('focus', this.handleFocus.bind(this));
- this.textarea.addEventListener('blur', this.handleBlur.bind(this));
- }
-
- /**
- * Handle focus event
- * @private
- */
- handleFocus() {
- this.container.classList.add('focused');
- }
-
- /**
- * Handle blur event
- * @private
- */
- handleBlur() {
- this.container.classList.remove('focused');
- this.validate();
- }
-
- /**
- * Handle formula change
- * @private
- */
- handleChange() {
- if (typeof this.options.on_change === 'function') {
- this.options.on_change(this.formula, this._referenced_fields);
- }
- }
-
- /**
- * Set the formula value
- * @param {string} value - Formula to set
- * @public
- */
- setValue(value) {
- this.formula = value || '';
-
- if (this.textarea) {
- this.textarea.value = this.formula;
- }
-
- this.validate();
- }
-
- /**
- * Get the current formula value
- * @returns {string} Current formula
- * @public
- */
- getValue() {
- return this.formula;
- }
-
- /**
- * Validate the current formula
- * @returns {boolean} Whether the formula is valid
- * @public
- */
- validate() {
- // Clear previous errors
- this.errors = [];
-
- // Skip validation for empty formula
- if (!this.formula.trim()) {
- this.hideError();
- return true;
- }
-
- // Validate formula syntax
- const validation = validateFormula(this.formula);
-
- if (!validation.isValid) {
- this.errors.push(validation.error);
- this.showError(validation.error);
- return false;
- }
-
- // Get referenced fields
- this._referenced_fields = extractReferencedFields(
- this.formula,
- this.fields.map(f => f.fieldname)
- );
-
- // Hide error if validation passed
- this.hideError();
- return true;
- }
-
- /**
- * Show error message
- * @param {string} message - Error message
- * @private
- */
- showError(message) {
- if (!this.errorEl) return;
-
- this.errorEl.textContent = formatErrorMessage(message);
- this.errorEl.classList.remove('hidden');
- this.container.classList.add('has-error');
- }
-
- /**
- * Hide error message
- * @private
- */
- hideError() {
- if (!this.errorEl) return;
-
- this.errorEl.textContent = '';
- this.errorEl.classList.add('hidden');
- this.container.classList.remove('has-error');
- }
-
- /**
- * Evaluate the current formula
- * @param {Object} [context] - Additional context for evaluation
- * @returns {*} Result of the evaluation
- * @public
- */
- evaluate(context = {}) {
- if (!this.formula.trim()) return null;
-
- // Validate before evaluation
- if (!this.validate()) {
- return null;
- }
-
- // Create evaluation context
- const evalContext = {
- doc: this.options.doc || {},
- ...context
- };
-
- try {
- return safeEval(this.formula, evalContext);
- } catch (error) {
- console.error('Error evaluating formula:', error);
- this.showError(error.message);
- return null;
- }
- }
-
- /**
- * Gets the fields referenced in the current formula
- * @returns {Array} Array of referenced field names
- * @public
- */
- getReferencedFields() {
- return [...this._referenced_fields];
- }
-
- /**
- * Destroy the editor and clean up
- * @public
- */
- destroy() {
- // Remove event listeners
- if (this.textarea) {
- this.textarea.removeEventListener('input', this.debouncedOnChange);
- this.textarea.removeEventListener('focus', this.handleFocus);
- this.textarea.removeEventListener('blur', this.handleBlur);
- }
-
- // Remove DOM elements
- if (this.container && this.container.parentNode) {
- this.container.parentNode.removeChild(this.container);
- }
-
- // Clean up properties
- this.initialized = false;
- this._cached_fields = null;
- this._field_options = null;
- this._referenced_fields = [];
- }
-}
-
-export default FormulaEditor;
\ No newline at end of file
diff --git a/formula_editor/public/js/index.js b/formula_editor/public/js/index.js
deleted file mode 100644
index d5bded0..0000000
--- a/formula_editor/public/js/index.js
+++ /dev/null
@@ -1,376 +0,0 @@
-/**
- * Formula Editor
- * Main entry point for the Formula Editor module
- */
-
-import FormulaEditor from './core';
-import FormulaEditorUI from './ui';
-import FormulaEditorTriggers from './triggers';
-import FormulaEditorUniversalFields from './universal_fields';
-import FormulaEditorTableRows from './table_row_formulas';
-import FormulaEditorReportFields from './report_fields';
-import * as utils from './utils';
-
-// Create the top-level namespace for the formula editor
-frappe.formula_editor = {
- // Core classes
- FormulaEditor,
- FormulaEditorUI,
- FormulaEditorTriggers,
- FormulaEditorUniversalFields,
- FormulaEditorTableRows,
- FormulaEditorReportFields,
-
- // Utilities
- utils,
-
- // Instances cache
- _instances: {},
-
- /**
- * Create a new formula editor instance
- * @param {Object} options - Configuration options
- * @returns {FormulaEditor} Formula editor instance
- */
- create(options) {
- const editor = new FormulaEditor(options);
- const id = options.id || `formula_editor_${Math.random().toString(36).substr(2, 9)}`;
-
- this._instances[id] = editor;
- return editor;
- },
-
- /**
- * Get a formula editor instance by ID
- * @param {string} id - Instance ID
- * @returns {FormulaEditor|null} Formula editor instance or null if not found
- */
- get(id) {
- return this._instances[id] || null;
- },
-
- /**
- * Destroy a formula editor instance
- * @param {string} id - Instance ID
- * @returns {boolean} Success status
- */
- destroy(id) {
- const editor = this.get(id);
- if (!editor) return false;
-
- editor.destroy();
- delete this._instances[id];
- return true;
- },
-
- /**
- * Initialize formula editor for a form field
- * @param {frappe.ui.form.Field} field - Form field to initialize editor for
- * @param {Object} [options={}] - Additional options
- * @returns {FormulaEditor} Formula editor instance
- */
- initForField(field, options = {}) {
- if (!field || !field.frm || !field.df) {
- console.error('Invalid field provided to initForField');
- return null;
- }
-
- const fieldId = `${field.frm.doctype}_${field.df.fieldname}`;
-
- // Check if editor already exists for this field
- const existingEditor = this.get(fieldId);
- if (existingEditor) return existingEditor;
-
- // Create wrapper element
- const $wrapper = $('
');
- field.$wrapper.find('.control-input-wrapper').append($wrapper);
-
- // Create editor
- const editorOptions = {
- id: fieldId,
- parent: $wrapper[0],
- doctype: field.frm.doctype,
- fieldname: field.df.fieldname,
- doc: field.frm.doc,
- on_change: (value, referencedFields) => {
- field.set_input(value);
-
- // Store referenced fields to update formula when they change
- if (Array.isArray(referencedFields)) {
- field._formulaReferencedFields = referencedFields;
- }
- },
- ...options
- };
-
- const editor = this.create(editorOptions);
-
- // Set up field change listener to keep formula in sync
- field._originalRefresh = field.refresh;
- field.refresh = () => {
- const result = field._originalRefresh();
-
- // Update editor value if it doesn't match field value
- if (editor.getValue() !== field.value) {
- editor.setValue(field.value);
- }
-
- return result;
- };
-
- // Set up listener for referenced fields
- this._setupReferencedFieldsListeners(field.frm, field);
-
- return editor;
- },
-
- /**
- * Set up listeners for fields referenced in a formula
- * @param {frappe.ui.form.Form} frm - Form instance
- * @param {frappe.ui.form.Field} formulaField - Formula field
- * @private
- */
- _setupReferencedFieldsListeners(frm, formulaField) {
- if (!frm || !formulaField || !formulaField._formulaReferencedFields) return;
-
- const referencedFields = formulaField._formulaReferencedFields;
-
- referencedFields.forEach(fieldname => {
- const field = frm.fields_dict[fieldname];
- if (!field) return;
-
- // Skip if already set up
- if (field._formulaListenerSetup) return;
-
- // Set up change listener
- const originalOnChange = field.df.change;
- field.df.change = () => {
- if (typeof originalOnChange === 'function') {
- originalOnChange();
- }
-
- // Update all formula fields that reference this field
- Object.values(frm.fields_dict).forEach(f => {
- if (f._formulaReferencedFields &&
- f._formulaReferencedFields.includes(fieldname) &&
- f !== formulaField) {
-
- // Get the editor
- const editorId = `${frm.doctype}_${f.df.fieldname}`;
- const editor = frappe.formula_editor.get(editorId);
-
- if (editor) {
- // Re-evaluate the formula
- const result = editor.evaluate();
- if (result !== undefined) {
- f.set_input(result);
- }
- }
- }
- });
- };
-
- field._formulaListenerSetup = true;
- });
- },
-
- /**
- * Initialize formula editor for a report
- * @param {frappe.views.ReportView|frappe.query_reports.QueryReport} report - Report instance
- * @param {string} reportName - Report name
- * @param {string} [reportType='report'] - Report type
- * @returns {FormulaEditorReportFields} Report fields handler instance
- */
- initForReport(report, reportName, reportType = 'report') {
- if (!report || !reportName) {
- console.error('Invalid report provided to initForReport');
- return null;
- }
-
- const reportId = `${reportType}_${reportName}`;
-
- // Check if already initialized
- if (report._formulaEditor) return report._formulaEditor;
-
- // Create a minimal editor instance for the report
- const editorOptions = {
- id: reportId,
- doctype: reportName,
- is_report: true
- };
-
- const editor = new FormulaEditor(editorOptions);
-
- // Create report fields handler
- const reportFields = new FormulaEditorReportFields({
- editor: editor,
- reportFormulas: []
- });
-
- // Integrate with the report
- reportFields.integrateWithReport(report, reportName, reportType);
-
- // Store reference on the report
- report._formulaEditor = reportFields;
-
- return reportFields;
- },
-
- /**
- * Initialize table row formulas for a form
- * @param {frappe.ui.form.Form} frm - Form instance
- * @param {Object} [options={}] - Additional options
- * @returns {FormulaEditorTableRows} Table rows handler instance
- */
- initTableFormulas(frm, options = {}) {
- if (!frm || !frm.doctype) {
- console.error('Invalid form provided to initTableFormulas');
- return null;
- }
-
- const formId = `table_formulas_${frm.doctype}`;
-
- // Check if already initialized
- if (frm._tableFormulasEditor) return frm._tableFormulasEditor;
-
- // Create a minimal editor instance for the form
- const editorOptions = {
- id: formId,
- doctype: frm.doctype,
- doc: frm.doc
- };
-
- const editor = new FormulaEditor(editorOptions);
-
- // Create table rows handler
- const tableRows = new FormulaEditorTableRows({
- editor: editor,
- tableFormulas: options.tableFormulas || [],
- onChange: options.onChange
- });
-
- // Attach to the form
- tableRows.attachToForm(frm);
-
- // Store reference on the form
- frm._tableFormulasEditor = tableRows;
-
- return tableRows;
- },
-
- /**
- * Initialize triggers for a form
- * @param {frappe.ui.form.Form} frm - Form instance
- * @param {Object} [options={}] - Additional options
- * @returns {FormulaEditorTriggers} Triggers handler instance
- */
- initTriggers(frm, options = {}) {
- if (!frm || !frm.doctype) {
- console.error('Invalid form provided to initTriggers');
- return null;
- }
-
- const formId = `triggers_${frm.doctype}`;
-
- // Check if already initialized
- if (frm._triggersEditor) return frm._triggersEditor;
-
- // Create a minimal editor instance for the form
- const editorOptions = {
- id: formId,
- doctype: frm.doctype,
- doc: frm.doc,
- is_trigger: true
- };
-
- const editor = new FormulaEditor(editorOptions);
-
- // Create triggers handler
- const triggers = new FormulaEditorTriggers({
- editor: editor,
- triggers: options.triggers || [],
- onChange: options.onChange
- });
-
- // Attach to the form
- triggers.attachToForm(frm);
-
- // Store reference on the form
- frm._triggersEditor = triggers;
-
- return triggers;
- },
-
- /**
- * Make a field into a formula editor
- * @param {frappe.ui.form.Form} frm - Form instance
- * @param {string} fieldname - Field to convert
- * @param {Object} [options={}] - Additional options
- */
- makeFieldFormula(frm, fieldname, options = {}) {
- if (!frm || !fieldname) return;
-
- const field = frm.get_field(fieldname);
- if (!field) return;
-
- // Add formula button to the field
- if (!field.$wrapper.find('.formula-editor-btn').length) {
- const $btn = $(`
-
- `);
-
- field.$wrapper.find('.control-input-wrapper').append($btn);
-
- // Show formula editor on click
- $btn.on('click', () => {
- this.showFieldFormulaDialog(frm, fieldname, options);
- });
- }
- },
-
- /**
- * Show formula editor dialog for a field
- * @param {frappe.ui.form.Form} frm - Form instance
- * @param {string} fieldname - Field name
- * @param {Object} [options={}] - Additional options
- */
- showFieldFormulaDialog(frm, fieldname, options = {}) {
- const field = frm.get_field(fieldname);
- if (!field) return;
-
- // Create dialog
- const dialog = new frappe.ui.Dialog({
- title: __('Formula Editor: {0}', [field.df.label || fieldname]),
- fields: [
- {
- fieldtype: 'Code',
- fieldname: 'formula',
- label: __('Formula'),
- options: 'JavaScript',
- default: field.value || ''
- }
- ],
- primary_action_label: __('Apply'),
- primary_action: (values) => {
- field.set_input(values.formula);
- dialog.hide();
- }
- });
-
- dialog.show();
- }
-};
-
-// Register setup hooks
-$(document).on('form-load', function(event, frm) {
- // Find formula fields and convert them to formula editors
- frm.meta.fields.forEach(df => {
- if (df.fieldtype === 'Code' && df.options === 'Formula') {
- frappe.formula_editor.makeFieldFormula(frm, df.fieldname);
- }
- });
-});
-
-export default frappe.formula_editor;
\ No newline at end of file
diff --git a/formula_editor/public/js/report_fields.js b/formula_editor/public/js/report_fields.js
deleted file mode 100644
index 53882e2..0000000
--- a/formula_editor/public/js/report_fields.js
+++ /dev/null
@@ -1,1161 +0,0 @@
-/**
- * Formula Editor Report Fields
- * Handles formula functionality for reports and analytical views
- */
-
-import {
- safeEval,
- validateFormula,
- formatErrorMessage,
- debounce,
- isEmpty,
- deepClone,
- showToast
-} from './utils';
-
-class FormulaEditorReportFields {
- /**
- * Create a new FormulaEditorReportFields instance
- * @param {Object} options - Configuration options
- * @param {Object} options.editor - Reference to the main FormulaEditor instance
- * @param {Array} [options.reportFormulas=[]] - Initial report formulas
- * @param {Function} [options.onChange] - Callback for formula changes
- */
- constructor(options) {
- this.options = Object.assign({
- editor: null,
- reportFormulas: [],
- onChange: null
- }, options);
-
- // Validate required options
- if (!this.options.editor) {
- throw new Error(__('Editor instance is required for FormulaEditorReportFields'));
- }
-
- // Initialize properties
- this.editor = this.options.editor;
- this.reportFormulas = deepClone(this.options.reportFormulas || []);
- this.debouncedOnChange = debounce(this.handleChange.bind(this), 300);
-
- // Initialize
- this.init();
- }
-
- /**
- * Initialize report formulas
- * @private
- */
- init() {
- // Ensure all report formulas have proper structure
- this.validateReportFormulas();
- }
-
- /**
- * Validate report formulas structure
- * @private
- */
- validateReportFormulas() {
- if (!Array.isArray(this.reportFormulas)) {
- this.reportFormulas = [];
- return;
- }
-
- // Process each report formula to ensure it has all required properties
- this.reportFormulas = this.reportFormulas.map(formula => {
- // Skip if not an object
- if (typeof formula !== 'object' || formula === null) {
- return null;
- }
-
- // Ensure required properties
- return {
- id: formula.id || `report_formula_${Date.now()}`,
- name: formula.name || __('Unnamed Report Formula'),
- formula: formula.formula || '',
- reportType: formula.reportType || 'report', // 'report', 'query-report', 'custom'
- reportName: formula.reportName || '',
- fieldName: formula.fieldName || '',
- columnLabel: formula.columnLabel || formula.fieldName || __('Calculated'),
- position: formula.position || 'end', // 'start', 'end', after:fieldname
- positionField: formula.positionField || null,
- is_numeric: typeof formula.is_numeric === 'boolean' ? formula.is_numeric : true,
- format: formula.format || null, // 'Currency', 'Percent', etc.
- enabled: typeof formula.enabled === 'boolean' ? formula.enabled : true,
- description: formula.description || '',
- precision: formula.precision !== undefined ? formula.precision : 2,
- aggregation: formula.aggregation || null // 'sum', 'avg', 'min', 'max', 'count'
- };
- }).filter(Boolean); // Remove invalid formulas
- }
-
- /**
- * Get all report formulas
- * @returns {Array} List of report formulas
- * @public
- */
- getReportFormulas() {
- return deepClone(this.reportFormulas);
- }
-
- /**
- * Get report formulas for a specific report
- * @param {string} reportName - Report name
- * @param {string} [reportType='report'] - Report type ('report', 'query-report', 'custom')
- * @returns {Array} List of formulas for the report
- * @public
- */
- getFormulasForReport(reportName, reportType = 'report') {
- return this.reportFormulas.filter(f =>
- f.reportName === reportName && f.reportType === reportType
- ).map(f => deepClone(f));
- }
-
- /**
- * Get a report formula by ID
- * @param {string} id - Formula ID
- * @returns {Object|null} Formula object or null if not found
- * @public
- */
- getReportFormula(id) {
- const formula = this.reportFormulas.find(f => f.id === id);
- return formula ? deepClone(formula) : null;
- }
-
- /**
- * Add a new report formula
- * @param {Object} formula - Formula data
- * @returns {string} New formula ID
- * @public
- */
- addReportFormula(formula = {}) {
- if (!formula.reportName) {
- throw new Error(__('Report name is required'));
- }
-
- const id = formula.id || `report_formula_${Date.now()}`;
-
- const newFormula = {
- id: id,
- name: formula.name || __('Unnamed Report Formula'),
- formula: formula.formula || '',
- reportType: formula.reportType || 'report', // 'report', 'query-report', 'custom'
- reportName: formula.reportName,
- fieldName: formula.fieldName || `calculated_${id.substring(id.length - 6)}`,
- columnLabel: formula.columnLabel || formula.fieldName || __('Calculated'),
- position: formula.position || 'end', // 'start', 'end', after:fieldname
- positionField: formula.positionField || null,
- is_numeric: typeof formula.is_numeric === 'boolean' ? formula.is_numeric : true,
- format: formula.format || null, // 'Currency', 'Percent', etc.
- enabled: typeof formula.enabled === 'boolean' ? formula.enabled : true,
- description: formula.description || '',
- precision: formula.precision !== undefined ? formula.precision : 2,
- aggregation: formula.aggregation || null // 'sum', 'avg', 'min', 'max', 'count'
- };
-
- this.reportFormulas.push(newFormula);
- this.debouncedOnChange();
-
- return newFormula.id;
- }
-
- /**
- * Update a report formula
- * @param {string} id - Formula ID
- * @param {Object} updates - Properties to update
- * @returns {boolean} Success status
- * @public
- */
- updateReportFormula(id, updates) {
- const index = this.reportFormulas.findIndex(f => f.id === id);
- if (index === -1) return false;
-
- // Clone the formula
- const formula = deepClone(this.reportFormulas[index]);
-
- // Apply updates
- Object.keys(updates).forEach(key => {
- if (key !== 'id') { // Don't allow changing the ID
- formula[key] = updates[key];
- }
- });
-
- // Update the formula
- this.reportFormulas[index] = formula;
- this.debouncedOnChange();
-
- return true;
- }
-
- /**
- * Delete a report formula
- * @param {string} id - Formula ID
- * @returns {boolean} Success status
- * @public
- */
- deleteReportFormula(id) {
- const index = this.reportFormulas.findIndex(f => f.id === id);
- if (index === -1) return false;
-
- this.reportFormulas.splice(index, 1);
- this.debouncedOnChange();
-
- return true;
- }
-
- /**
- * Enable/disable a report formula
- * @param {string} id - Formula ID
- * @param {boolean} enabled - Whether to enable the formula
- * @returns {boolean} Success status
- * @public
- */
- setReportFormulaEnabled(id, enabled) {
- return this.updateReportFormula(id, { enabled: !!enabled });
- }
-
- /**
- * Validate a report formula
- * @param {string} id - Formula ID, or formula string if testOnly is true
- * @param {boolean} [testOnly=false] - Whether to validate a formula string instead of a saved formula
- * @returns {Object} Validation result with isValid and error
- * @public
- */
- validateReportFormula(id, testOnly = false) {
- let formula;
-
- if (testOnly) {
- formula = id; // In this case, id is actually the formula string
- } else {
- const formulaObj = this.getReportFormula(id);
- if (!formulaObj) {
- return { isValid: false, error: __('Formula not found') };
- }
- formula = formulaObj.formula;
- }
-
- if (!formula || typeof formula !== 'string' || !formula.trim()) {
- return { isValid: true, error: null };
- }
-
- // Use basic formula validation
- return validateFormula(formula);
- }
-
- /**
- * Apply formulas to report data
- * @param {Array} data - Report data rows
- * @param {Array} columns - Report columns
- * @param {string} reportName - Report name
- * @param {string} [reportType='report'] - Report type ('report', 'query-report', 'custom')
- * @returns {Object} Modified data and columns
- * @public
- */
- applyFormulasToReport(data, columns, reportName, reportType = 'report') {
- try {
- if (!Array.isArray(data) || !Array.isArray(columns) || !reportName) {
- return { data, columns };
- }
-
- // Get formulas for this report
- const formulas = this.getFormulasForReport(reportName, reportType)
- .filter(f => f.enabled);
-
- if (formulas.length === 0) {
- return { data, columns };
- }
-
- // Create a column map for easy lookup
- const columnMap = {};
- columns.forEach((col, index) => {
- columnMap[col.fieldname] = index;
- });
-
- // Process each formula
- const newData = deepClone(data);
- const newColumns = deepClone(columns);
-
- formulas.forEach(formula => {
- try {
- // Check if column already exists
- const existingColumnIndex = columnMap[formula.fieldName];
-
- // Create new column if needed
- if (existingColumnIndex === undefined) {
- const newColumn = {
- fieldname: formula.fieldName,
- label: formula.columnLabel,
- fieldtype: formula.is_numeric ? 'Float' : 'Data',
- options: formula.format,
- precision: formula.precision,
- width: 120,
- is_formula: true,
- formula_reference: formula.id
- };
-
- // Determine position of the new column
- if (formula.position === 'start') {
- newColumns.unshift(newColumn);
-
- // Update column map
- Object.keys(columnMap).forEach(key => {
- columnMap[key]++;
- });
- columnMap[formula.fieldName] = 0;
- } else if (formula.position === 'end' || !formula.positionField) {
- newColumns.push(newColumn);
- columnMap[formula.fieldName] = newColumns.length - 1;
- } else {
- // Position after specific field
- const posIndex = columnMap[formula.positionField];
- if (posIndex !== undefined) {
- newColumns.splice(posIndex + 1, 0, newColumn);
-
- // Update column map
- Object.keys(columnMap).forEach(key => {
- if (columnMap[key] > posIndex) {
- columnMap[key]++;
- }
- });
- columnMap[formula.fieldName] = posIndex + 1;
- } else {
- // Fallback to end
- newColumns.push(newColumn);
- columnMap[formula.fieldName] = newColumns.length - 1;
- }
- }
- }
-
- // Apply formula to each row
- newData.forEach((row, rowIndex) => {
- try {
- const context = {
- row: row,
- data: newData,
- rowIndex: rowIndex,
- columns: newColumns,
- columnMap: columnMap,
- reportName: reportName,
- reportType: reportType
- };
-
- const result = this.evaluateReportFormula(formula.formula, context);
-
- // Find where to put the result
- const targetIdx = columnMap[formula.fieldName];
-
- if (targetIdx !== undefined) {
- // If row is an array, extend it if needed
- if (Array.isArray(row)) {
- if (row.length <= targetIdx) {
- while (row.length < targetIdx) {
- row.push(null);
- }
- row.push(result);
- } else {
- row[targetIdx] = result;
- }
- }
- // If row is an object, add/update the property
- else if (typeof row === 'object' && row !== null) {
- row[formula.fieldName] = result;
- }
- }
- } catch (rowError) {
- console.error(`Error applying formula to row ${rowIndex}:`, rowError);
- // Set error value
- const targetIdx = columnMap[formula.fieldName];
- if (targetIdx !== undefined) {
- if (Array.isArray(row)) {
- row[targetIdx] = '#ERROR';
- } else if (typeof row === 'object' && row !== null) {
- row[formula.fieldName] = '#ERROR';
- }
- }
- }
- });
-
- // Apply aggregation if specified
- if (formula.aggregation && formula.is_numeric) {
- this.applyAggregation(newData, formula);
- }
-
- } catch (formulaError) {
- console.error(`Error processing formula ${formula.id}:`, formulaError);
- }
- });
-
- return { data: newData, columns: newColumns };
-
- } catch (error) {
- console.error('Error applying formulas to report:', error);
- showToast(__('Error applying formulas to report: {0}', [formatErrorMessage(error)]), 'error');
- return { data, columns };
- }
- }
-
- /**
- * Evaluate a formula in the context of a report row
- * @param {string} formula - Formula to evaluate
- * @param {Object} context - Evaluation context
- * @returns {*} Evaluation result
- * @private
- */
- evaluateReportFormula(formula, context) {
- return safeEval(formula, context);
- }
-
- /**
- * Apply aggregation to a formula field
- * @param {Array} data - Report data
- * @param {Object} formula - Formula configuration
- * @private
- */
- applyAggregation(data, formula) {
- if (!Array.isArray(data) || data.length === 0) return;
-
- const fieldName = formula.fieldName;
- let result;
-
- switch (formula.aggregation) {
- case 'sum':
- result = this.calculateSum(data, fieldName);
- break;
- case 'avg':
- result = this.calculateAverage(data, fieldName);
- break;
- case 'min':
- result = this.calculateMin(data, fieldName);
- break;
- case 'max':
- result = this.calculateMax(data, fieldName);
- break;
- case 'count':
- result = this.calculateCount(data, fieldName);
- break;
- default:
- return;
- }
-
- // Look for total row (last row or one with is_total=true)
- let totalRow = null;
-
- for (let i = data.length - 1; i >= 0; i--) {
- const row = data[i];
-
- if (typeof row === 'object' && row !== null && row.is_total_row) {
- totalRow = row;
- break;
- }
- }
-
- // If no total row found, create one
- if (!totalRow) {
- totalRow = { is_total_row: true };
- data.push(totalRow);
- }
-
- // Set the aggregated value
- if (typeof totalRow === 'object') {
- totalRow[fieldName] = result;
- }
- }
-
- /**
- * Calculate sum of a field across all rows
- * @param {Array} data - Report data
- * @param {string} fieldName - Field to sum
- * @returns {number} Sum value
- * @private
- */
- calculateSum(data, fieldName) {
- let sum = 0;
-
- data.forEach(row => {
- if (row.is_total_row) return; // Skip total rows
-
- const value = this.getRowValue(row, fieldName);
- if (typeof value === 'number' && !isNaN(value)) {
- sum += value;
- }
- });
-
- return sum;
- }
-
- /**
- * Calculate average of a field across all rows
- * @param {Array} data - Report data
- * @param {string} fieldName - Field to average
- * @returns {number} Average value
- * @private
- */
- calculateAverage(data, fieldName) {
- let sum = 0;
- let count = 0;
-
- data.forEach(row => {
- if (row.is_total_row) return; // Skip total rows
-
- const value = this.getRowValue(row, fieldName);
- if (typeof value === 'number' && !isNaN(value)) {
- sum += value;
- count++;
- }
- });
-
- return count > 0 ? sum / count : 0;
- }
-
- /**
- * Calculate minimum value of a field across all rows
- * @param {Array} data - Report data
- * @param {string} fieldName - Field to check
- * @returns {number} Minimum value
- * @private
- */
- calculateMin(data, fieldName) {
- let min = Infinity;
-
- data.forEach(row => {
- if (row.is_total_row) return; // Skip total rows
-
- const value = this.getRowValue(row, fieldName);
- if (typeof value === 'number' && !isNaN(value) && value < min) {
- min = value;
- }
- });
-
- return min === Infinity ? 0 : min;
- }
-
- /**
- * Calculate maximum value of a field across all rows
- * @param {Array} data - Report data
- * @param {string} fieldName - Field to check
- * @returns {number} Maximum value
- * @private
- */
- calculateMax(data, fieldName) {
- let max = -Infinity;
-
- data.forEach(row => {
- if (row.is_total_row) return; // Skip total rows
-
- const value = this.getRowValue(row, fieldName);
- if (typeof value === 'number' && !isNaN(value) && value > max) {
- max = value;
- }
- });
-
- return max === -Infinity ? 0 : max;
- }
-
- /**
- * Count non-empty values of a field across all rows
- * @param {Array} data - Report data
- * @param {string} fieldName - Field to count
- * @returns {number} Count of non-empty values
- * @private
- */
- calculateCount(data, fieldName) {
- let count = 0;
-
- data.forEach(row => {
- if (row.is_total_row) return; // Skip total rows
-
- const value = this.getRowValue(row, fieldName);
- if (value !== null && value !== undefined && value !== '') {
- count++;
- }
- });
-
- return count;
- }
-
- /**
- * Get a value from a row, handling both array and object formats
- * @param {Object|Array} row - Report row
- * @param {string} fieldName - Field name
- * @returns {*} Field value
- * @private
- */
- getRowValue(row, fieldName) {
- if (Array.isArray(row)) {
- // Find the column index
- const columns = this.options.editor.reportColumns || [];
- const colIndex = columns.findIndex(c => c.fieldname === fieldName);
-
- return colIndex !== -1 ? row[colIndex] : null;
- } else if (typeof row === 'object' && row !== null) {
- return row[fieldName];
- }
-
- return null;
- }
-
- /**
- * Integrate with a Frappe report
- * @param {Object} report - Frappe report instance
- * @param {string} reportName - Report name
- * @param {string} [reportType='report'] - Report type
- * @returns {boolean} Success status
- * @public
- */
- integrateWithReport(report, reportName, reportType = 'report') {
- if (!report || !reportName) {
- return false;
- }
-
- try {
- // Store reference to the report
- this.currentReport = report;
- this.currentReportName = reportName;
- this.currentReportType = reportType;
-
- // Save original functions
- if (!report._original_get_data) {
- report._original_get_data = report.get_data;
-
- // Override get_data to apply formulas
- report.get_data = (...args) => {
- const result = report._original_get_data.apply(report, args);
-
- if (result && typeof result.then === 'function') {
- // It's a promise
- return result.then(response => {
- const data = response.data || response;
- const columns = response.columns || report.columns;
-
- // Apply formulas to the data
- const modified = this.applyFormulasToReport(
- data,
- columns,
- reportName,
- reportType
- );
-
- // Update the response
- if (typeof response === 'object') {
- response.data = modified.data;
- response.columns = modified.columns;
- return response;
- }
-
- return modified.data;
- });
- } else {
- // It's direct data
- const data = result || [];
- const columns = report.columns || [];
-
- // Apply formulas to the data
- const modified = this.applyFormulasToReport(
- data,
- columns,
- reportName,
- reportType
- );
-
- // Update the report columns
- report.columns = modified.columns;
-
- return modified.data;
- }
- };
- }
-
- // Also hook into refresh if available
- if (report.refresh && !report._original_refresh) {
- report._original_refresh = report.refresh;
-
- report.refresh = () => {
- // Call original refresh
- const result = report._original_refresh.apply(report);
-
- // Add formula controls to the report
- setTimeout(() => {
- this.addFormulaControlsToReport(report, reportName, reportType);
- }, 500);
-
- return result;
- };
- }
-
- return true;
-
- } catch (error) {
- console.error('Error integrating with report:', error);
- return false;
- }
- }
-
- /**
- * Add formula controls to a report UI
- * @param {Object} report - Frappe report instance
- * @param {string} reportName - Report name
- * @param {string} reportType - Report type
- * @private
- */
- addFormulaControlsToReport(report, reportName, reportType) {
- try {
- // Check if controls already exist
- if (report.$formulaControls) return;
-
- // Find report toolbar
- const $toolbar = $(report.wrapper).find('.report-action-buttons, .grid-report-action-buttons');
- if (!$toolbar.length) return;
-
- // Create formula button
- const $formulaBtn = $(`
-
- `);
-
- // Add button to toolbar
- $toolbar.append($formulaBtn);
-
- // Store reference
- report.$formulaControls = $formulaBtn;
-
- // Add click handler
- $formulaBtn.on('click', () => {
- this.showReportFormulaDialog(reportName, reportType, report);
- });
-
- } catch (error) {
- console.error('Error adding formula controls to report:', error);
- }
- }
-
- /**
- * Show dialog to manage report formulas
- * @param {string} reportName - Report name
- * @param {string} reportType - Report type
- * @param {Object} report - Report instance
- * @private
- */
- showReportFormulaDialog(reportName, reportType, report) {
- const formulas = this.getFormulasForReport(reportName, reportType);
-
- // Get available columns
- const columns = report.columns || [];
- const columnOptions = columns.map(col => ({
- label: col.label || col.fieldname,
- value: col.fieldname
- }));
-
- // Create dialog
- const dialog = new frappe.ui.Dialog({
- title: __('Report Formulas'),
- fields: [
- {
- fieldtype: 'HTML',
- fieldname: 'formula_list',
- label: __('Formulas'),
- options: ``
- },
- {
- fieldtype: 'Section Break',
- label: __('Add New Formula')
- },
- {
- fieldtype: 'Data',
- fieldname: 'name',
- label: __('Name'),
- reqd: true
- },
- {
- fieldtype: 'Code',
- fieldname: 'formula',
- label: __('Formula'),
- options: 'JavaScript',
- description: __('Use row to access the current row. Example: row.amount * 1.1')
- },
- {
- fieldtype: 'Column Break'
- },
- {
- fieldtype: 'Data',
- fieldname: 'fieldName',
- label: __('Field Name'),
- description: __('Unique identifier for this formula field')
- },
- {
- fieldtype: 'Data',
- fieldname: 'columnLabel',
- label: __('Column Label'),
- description: __('Display label for the column')
- },
- {
- fieldtype: 'Section Break'
- },
- {
- fieldtype: 'Select',
- fieldname: 'position',
- label: __('Position'),
- options: [
- { label: __('At Start'), value: 'start' },
- { label: __('At End'), value: 'end' },
- { label: __('After Field'), value: 'after' }
- ],
- default: 'end'
- },
- {
- fieldtype: 'Select',
- fieldname: 'positionField',
- label: __('After Field'),
- options: columnOptions,
- depends_on: 'eval:doc.position === "after"'
- },
- {
- fieldtype: 'Column Break'
- },
- {
- fieldtype: 'Check',
- fieldname: 'is_numeric',
- label: __('Numeric'),
- default: 1
- },
- {
- fieldtype: 'Select',
- fieldname: 'format',
- label: __('Format'),
- options: [
- { label: __('None'), value: '' },
- { label: __('Currency'), value: 'Currency' },
- { label: __('Percent'), value: 'Percent' },
- { label: __('Number'), value: 'Number' }
- ],
- default: ''
- },
- {
- fieldtype: 'Int',
- fieldname: 'precision',
- label: __('Precision'),
- default: 2
- },
- {
- fieldtype: 'Section Break'
- },
- {
- fieldtype: 'Select',
- fieldname: 'aggregation',
- label: __('Aggregation'),
- options: [
- { label: __('None'), value: '' },
- { label: __('Sum'), value: 'sum' },
- { label: __('Average'), value: 'avg' },
- { label: __('Minimum'), value: 'min' },
- { label: __('Maximum'), value: 'max' },
- { label: __('Count'), value: 'count' }
- ],
- default: ''
- },
- {
- fieldtype: 'Text',
- fieldname: 'description',
- label: __('Description')
- }
- ],
- primary_action_label: __('Add Formula'),
- primary_action: (values) => {
- // Generate field name if not provided
- if (!values.fieldName) {
- values.fieldName = `calculated_${Math.random().toString(36).substr(2, 6)}`;
- }
-
- // Add the formula
- this.addReportFormula({
- name: values.name,
- formula: values.formula,
- reportType: reportType,
- reportName: reportName,
- fieldName: values.fieldName,
- columnLabel: values.columnLabel || values.name,
- position: values.position,
- positionField: values.position === 'after' ? values.positionField : null,
- is_numeric: values.is_numeric,
- format: values.format,
- precision: values.precision,
- aggregation: values.aggregation,
- description: values.description
- });
-
- // Refresh formulas list
- this.refreshFormulasList(dialog);
-
- // Clear form
- dialog.set_values({
- name: '',
- formula: '',
- fieldName: '',
- columnLabel: '',
- description: ''
- });
-
- // Refresh report
- if (report && report.refresh) {
- report.refresh();
- }
- }
- });
-
- // Render formulas list
- dialog.show();
- this.refreshFormulasList(dialog);
- }
-
- /**
- * Refresh the formulas list in the dialog
- * @param {Object} dialog - Frappe dialog instance
- * @private
- */
- refreshFormulasList(dialog) {
- const $list = dialog.$wrapper.find('.formula-list');
- const formulas = this.getFormulasForReport(
- this.currentReportName,
- this.currentReportType
- );
-
- if (formulas.length === 0) {
- $list.html(`${__('No formulas defined for this report')}
`);
- return;
- }
-
- let html = '';
- $list.html(html);
-
- // Add event handlers
- $list.find('.formula-edit-btn').on('click', (e) => {
- const $item = $(e.target).closest('.formula-list-item');
- const id = $item.data('id');
- this.editReportFormula(id, dialog);
- });
-
- $list.find('.formula-toggle-btn').on('click', (e) => {
- const $item = $(e.target).closest('.formula-list-item');
- const id = $item.data('id');
- const formula = this.getReportFormula(id);
-
- if (formula) {
- this.setReportFormulaEnabled(id, !formula.enabled);
- this.refreshFormulasList(dialog);
-
- // Refresh report
- if (this.currentReport && this.currentReport.refresh) {
- this.currentReport.refresh();
- }
- }
- });
-
- $list.find('.formula-delete-btn').on('click', (e) => {
- const $item = $(e.target).closest('.formula-list-item');
- const id = $item.data('id');
-
- frappe.confirm(
- __('Delete this formula?'),
- () => {
- this.deleteReportFormula(id);
- this.refreshFormulasList(dialog);
-
- // Refresh report
- if (this.currentReport && this.currentReport.refresh) {
- this.currentReport.refresh();
- }
- }
- );
- });
- }
-
- /**
- * Edit a report formula
- * @param {string} id - Formula ID
- * @param {Object} dialog - Frappe dialog instance
- * @private
- */
- editReportFormula(id, dialog) {
- const formula = this.getReportFormula(id);
- if (!formula) return;
-
- // Switch dialog to edit mode
- dialog.set_title(__('Edit Formula'));
-
- // Set values
- dialog.set_values({
- name: formula.name,
- formula: formula.formula,
- fieldName: formula.fieldName,
- columnLabel: formula.columnLabel,
- position: formula.position,
- positionField: formula.positionField,
- is_numeric: formula.is_numeric,
- format: formula.format,
- precision: formula.precision,
- aggregation: formula.aggregation,
- description: formula.description
- });
-
- // Change primary action
- dialog.set_primary_action_label(__('Update Formula'));
-
- // Store original primary action
- const originalAction = dialog.primary_action;
-
- // Set new primary action
- dialog.primary_action = (values) => {
- // Update the formula
- this.updateReportFormula(id, {
- name: values.name,
- formula: values.formula,
- fieldName: values.fieldName,
- columnLabel: values.columnLabel || values.name,
- position: values.position,
- positionField: values.position === 'after' ? values.positionField : null,
- is_numeric: values.is_numeric,
- format: values.format,
- precision: values.precision,
- aggregation: values.aggregation,
- description: values.description
- });
-
- // Refresh formulas list
- this.refreshFormulasList(dialog);
-
- // Reset dialog
- dialog.set_title(__('Report Formulas'));
- dialog.set_primary_action_label(__('Add Formula'));
- dialog.primary_action = originalAction;
-
- // Clear form
- dialog.set_values({
- name: '',
- formula: '',
- fieldName: '',
- columnLabel: '',
- description: ''
- });
-
- // Refresh report
- if (this.currentReport && this.currentReport.refresh) {
- this.currentReport.refresh();
- }
- };
- }
-
- /**
- * Handle formula changes
- * @private
- */
- handleChange() {
- if (typeof this.options.onChange === 'function') {
- this.options.onChange(this.reportFormulas);
- }
- }
-
- /**
- * Import report formulas from JSON
- * @param {string|Object} json - JSON string or object
- * @returns {boolean} Success status
- * @public
- */
- importReportFormulas(json) {
- try {
- let data;
-
- if (typeof json === 'string') {
- data = JSON.parse(json);
- } else if (typeof json === 'object' && json !== null) {
- data = json;
- } else {
- throw new Error(__('Invalid import data format'));
- }
-
- if (!Array.isArray(data)) {
- throw new Error(__('Import data must be an array'));
- }
-
- // Validate each formula
- const validFormulas = data.filter(formula =>
- typeof formula === 'object' &&
- formula !== null &&
- typeof formula.reportName === 'string' &&
- typeof formula.fieldName === 'string'
- );
-
- if (validFormulas.length === 0) {
- throw new Error(__('No valid report formulas found in import data'));
- }
-
- // Replace report formulas
- this.reportFormulas = validFormulas.map(formula => ({
- id: formula.id || `report_formula_${Date.now()}`,
- name: formula.name || __('Unnamed Report Formula'),
- formula: formula.formula || '',
- reportType: formula.reportType || 'report',
- reportName: formula.reportName,
- fieldName: formula.fieldName,
- columnLabel: formula.columnLabel || formula.fieldName || __('Calculated'),
- position: formula.position || 'end',
- positionField: formula.positionField || null,
- is_numeric: typeof formula.is_numeric === 'boolean' ? formula.is_numeric : true,
- format: formula.format || null,
- enabled: typeof formula.enabled === 'boolean' ? formula.enabled : true,
- description: formula.description || '',
- precision: formula.precision !== undefined ? formula.precision : 2,
- aggregation: formula.aggregation || null
- }));
-
- this.debouncedOnChange();
- return true;
-
- } catch (error) {
- console.error('Error importing report formulas:', error);
- return false;
- }
- }
-
- /**
- * Export report formulas to JSON
- * @param {boolean} [prettyPrint=false] - Whether to pretty-print the JSON
- * @returns {string} JSON string
- * @public
- */
- exportReportFormulas(prettyPrint = false) {
- try {
- return JSON.stringify(this.reportFormulas, null, prettyPrint ? 2 : 0);
- } catch (error) {
- console.error('Error exporting report formulas:', error);
- return '[]';
- }
- }
-}
-
-export default FormulaEditorReportFields;
\ No newline at end of file
diff --git a/formula_editor/public/js/table_row_formulas.js b/formula_editor/public/js/table_row_formulas.js
deleted file mode 100644
index b41efdd..0000000
--- a/formula_editor/public/js/table_row_formulas.js
+++ /dev/null
@@ -1,851 +0,0 @@
-/**
- * Formula Editor Table Row Formulas
- * Handles formulas for table rows in documents
- */
-
-import {
- safeEval,
- validateFormula,
- extractReferencedFields,
- formatErrorMessage,
- debounce,
- isEmpty,
- deepClone,
- showToast
-} from './utils';
-
-class FormulaEditorTableRows {
- /**
- * Create a new FormulaEditorTableRows instance
- * @param {Object} options - Configuration options
- * @param {Object} options.editor - Reference to the main FormulaEditor instance
- * @param {Array} [options.tableFormulas=[]] - Initial table formulas
- * @param {Function} [options.onChange] - Callback for formula changes
- */
- constructor(options) {
- this.options = Object.assign({
- editor: null,
- tableFormulas: [],
- onChange: null
- }, options);
-
- // Validate required options
- if (!this.options.editor) {
- throw new Error(__('Editor instance is required for FormulaEditorTableRows'));
- }
-
- // Initialize properties
- this.editor = this.options.editor;
- this.tableFormulas = deepClone(this.options.tableFormulas || []);
- this.childTableFields = [];
- this.debouncedOnChange = debounce(this.handleChange.bind(this), 300);
-
- // Initialize
- this.init();
- }
-
- /**
- * Initialize table formulas
- * @private
- */
- init() {
- // Find all child table fields in the doctype
- this.loadChildTableFields();
-
- // Ensure all table formulas have proper structure
- this.validateTableFormulas();
- }
-
- /**
- * Load child table fields from the doctype
- * @private
- */
- loadChildTableFields() {
- const allFields = this.editor.fields || [];
-
- // Filter to only table type fields
- this.childTableFields = allFields.filter(field =>
- field.type === 'Table' && field.options
- );
- }
-
- /**
- * Validate table formulas structure and add missing properties
- * @private
- */
- validateTableFormulas() {
- if (!Array.isArray(this.tableFormulas)) {
- this.tableFormulas = [];
- return;
- }
-
- // Process each table formula to ensure it has all required properties
- this.tableFormulas = this.tableFormulas.map(formula => {
- // Skip if not an object
- if (typeof formula !== 'object' || formula === null) {
- return null;
- }
-
- // Check if table field exists
- const tableField = this.childTableFields.find(f => f.fieldname === formula.table_field);
- if (!tableField) {
- return null;
- }
-
- // Ensure required properties
- return {
- id: formula.id || `${formula.table_field}_${formula.target_field}_formula`,
- name: formula.name || __('Formula for {0}', [formula.target_field]),
- table_field: formula.table_field,
- target_field: formula.target_field,
- source_fields: Array.isArray(formula.source_fields) ? formula.source_fields : [],
- formula: formula.formula || '',
- child_doctype: tableField.options,
- enabled: typeof formula.enabled === 'boolean' ? formula.enabled : true,
- apply_on: formula.apply_on || 'change', // change, save, manual
- is_cumulative: !!formula.is_cumulative,
- description: formula.description || ''
- };
- }).filter(Boolean); // Remove invalid formulas
- }
-
- /**
- * Get all table formulas
- * @returns {Array} List of table formulas
- * @public
- */
- getTableFormulas() {
- return deepClone(this.tableFormulas);
- }
-
- /**
- * Get a table formula by ID
- * @param {string} id - Formula ID
- * @returns {Object|null} Formula object or null if not found
- * @public
- */
- getTableFormula(id) {
- const formula = this.tableFormulas.find(f => f.id === id);
- return formula ? deepClone(formula) : null;
- }
-
- /**
- * Get table formulas for a specific table field
- * @param {string} tableField - Table field name
- * @returns {Array} List of formulas for the table field
- * @public
- */
- getFormulasForTable(tableField) {
- return this.tableFormulas.filter(f => f.table_field === tableField).map(f => deepClone(f));
- }
-
- /**
- * Add a new table formula
- * @param {Object} formula - Formula data
- * @returns {string} New formula ID
- * @public
- */
- addTableFormula(formula = {}) {
- if (!formula.table_field || !formula.target_field) {
- throw new Error(__('Table field and target field are required'));
- }
-
- // Check if table field exists
- const tableField = this.childTableFields.find(f => f.fieldname === formula.table_field);
- if (!tableField) {
- throw new Error(__('Table field {0} not found', [formula.table_field]));
- }
-
- const newFormula = {
- id: formula.id || `${formula.table_field}_${formula.target_field}_formula`,
- name: formula.name || __('Formula for {0}', [formula.target_field]),
- table_field: formula.table_field,
- target_field: formula.target_field,
- source_fields: Array.isArray(formula.source_fields) ? deepClone(formula.source_fields) : [],
- formula: formula.formula || '',
- child_doctype: tableField.options,
- enabled: typeof formula.enabled === 'boolean' ? formula.enabled : true,
- apply_on: formula.apply_on || 'change',
- is_cumulative: !!formula.is_cumulative,
- description: formula.description || ''
- };
-
- this.tableFormulas.push(newFormula);
- this.debouncedOnChange();
-
- return newFormula.id;
- }
-
- /**
- * Update a table formula
- * @param {string} id - Formula ID
- * @param {Object} updates - Properties to update
- * @returns {boolean} Success status
- * @public
- */
- updateTableFormula(id, updates) {
- const index = this.tableFormulas.findIndex(f => f.id === id);
- if (index === -1) return false;
-
- // Clone the formula
- const formula = deepClone(this.tableFormulas[index]);
-
- // Apply updates
- Object.keys(updates).forEach(key => {
- // Handle special properties
- if (key === 'source_fields' && Array.isArray(updates.source_fields)) {
- formula.source_fields = deepClone(updates.source_fields);
- } else if (key === 'table_field') {
- // If table field is updated, verify it exists
- const tableField = this.childTableFields.find(f => f.fieldname === updates.table_field);
- if (tableField) {
- formula.table_field = updates.table_field;
- formula.child_doctype = tableField.options;
- }
- } else if (key !== 'id' && key !== 'child_doctype') { // Don't allow changing ID or child_doctype directly
- formula[key] = updates[key];
- }
- });
-
- // Update the formula
- this.tableFormulas[index] = formula;
- this.debouncedOnChange();
-
- return true;
- }
-
- /**
- * Delete a table formula
- * @param {string} id - Formula ID
- * @returns {boolean} Success status
- * @public
- */
- deleteTableFormula(id) {
- const index = this.tableFormulas.findIndex(f => f.id === id);
- if (index === -1) return false;
-
- this.tableFormulas.splice(index, 1);
- this.debouncedOnChange();
-
- return true;
- }
-
- /**
- * Enable/disable a table formula
- * @param {string} id - Formula ID
- * @param {boolean} enabled - Whether to enable the formula
- * @returns {boolean} Success status
- * @public
- */
- setTableFormulaEnabled(id, enabled) {
- return this.updateTableFormula(id, { enabled: !!enabled });
- }
-
- /**
- * Validate a table formula
- * @param {string} id - Formula ID
- * @returns {Object} Validation result with isValid and error
- * @public
- */
- validateTableFormula(id) {
- const formula = this.getTableFormula(id);
- if (!formula) {
- return { isValid: false, error: __('Formula not found') };
- }
-
- if (!formula.formula.trim()) {
- return { isValid: true, error: null };
- }
-
- // Use basic formula validation
- return validateFormula(formula.formula);
- }
-
- /**
- * Get field options for a child table
- * @param {string} childDoctype - Child doctype name
- * @returns {Promise} List of field options
- * @public
- */
- async getChildTableFields(childDoctype) {
- try {
- let meta;
-
- // Try to get meta from cache
- meta = frappe.get_meta(childDoctype);
-
- if (!meta) {
- // If not in cache, fetch it
- meta = await frappe.model.with_doctype(childDoctype);
- }
-
- if (!meta || !meta.fields) {
- throw new Error(__('Could not fetch metadata for {0}', [childDoctype]));
- }
-
- const fields = [];
-
- // Add fields from the child DocType
- for (const field of meta.fields) {
- // Skip certain field types
- if (['Section Break', 'Column Break', 'Tab Break', 'HTML', 'Button'].includes(field.fieldtype)) {
- continue;
- }
-
- fields.push({
- fieldname: field.fieldname,
- label: field.label || field.fieldname,
- type: field.fieldtype,
- options: field.options,
- is_child_table: field.fieldtype === 'Table'
- });
- }
-
- return fields;
-
- } catch (error) {
- console.error(`Error getting fields for child doctype ${childDoctype}:`, error);
- return [];
- }
- }
-
- /**
- * Apply formulas to a table
- * @param {Object} doc - Parent document
- * @param {string} tableField - Table field name
- * @param {Object} [options] - Options for applying formulas
- * @param {boolean} [options.silent=false] - Whether to suppress notifications
- * @returns {Promise