diff --git a/web_widget_float_formula/__init__.py b/web_widget_float_formula/__init__.py new file mode 100644 index 000000000..c71289ab1 --- /dev/null +++ b/web_widget_float_formula/__init__.py @@ -0,0 +1 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). diff --git a/web_widget_float_formula/__manifest__.py b/web_widget_float_formula/__manifest__.py new file mode 100644 index 000000000..c56adb15f --- /dev/null +++ b/web_widget_float_formula/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2014-2015 GRAP +# Copyright 2016 LasLabs Inc. +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + 'name': 'Web Widget - Formulas in Float Fields', + 'summary': 'Allow use of simple formulas in float fields', + 'version': '12.0.1.0.0', + 'category': 'Web', + 'author': + 'GRAP, LasLabs, Brainbean Apps, Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/web/', + 'license': 'AGPL-3', + 'depends': [ + 'web', + ], + 'data': [ + 'templates/assets.xml', + ], + 'installable': True, +} diff --git a/web_widget_float_formula/i18n/web_widget_float_formula.pot b/web_widget_float_formula/i18n/web_widget_float_formula.pot new file mode 100644 index 000000000..b662aad9b --- /dev/null +++ b/web_widget_float_formula/i18n/web_widget_float_formula.pot @@ -0,0 +1,14 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + diff --git a/web_widget_float_formula/readme/CONTRIBUTORS.rst b/web_widget_float_formula/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..7bcc9baf1 --- /dev/null +++ b/web_widget_float_formula/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Sylvain Le Gal (https://twitter.com/legalsylvain) +* Oleg Bulkin +* Alexey Pelykh diff --git a/web_widget_float_formula/readme/DESCRIPTION.rst b/web_widget_float_formula/readme/DESCRIPTION.rst new file mode 100644 index 000000000..ee4d5bb39 --- /dev/null +++ b/web_widget_float_formula/readme/DESCRIPTION.rst @@ -0,0 +1,11 @@ +This module allows the use of simple math formulas in corresponding fields: +``=45 + 4/3 - 5 * (2 + 1)`` + +Features: + +* ``+`` (addition) +* ``-`` (subtraction) +* ``*`` (multiplication) +* ``/`` (division) +* ``%`` (modulus) +* ``(`` and ``)`` parentheses diff --git a/web_widget_float_formula/readme/ROADMAP.rst b/web_widget_float_formula/readme/ROADMAP.rst new file mode 100644 index 000000000..51f124701 --- /dev/null +++ b/web_widget_float_formula/readme/ROADMAP.rst @@ -0,0 +1 @@ +This module is not needed for v13, as this feature is bundled with Odoo v13. diff --git a/web_widget_float_formula/static/description/icon.png b/web_widget_float_formula/static/description/icon.png new file mode 100644 index 000000000..7f744399c Binary files /dev/null and b/web_widget_float_formula/static/description/icon.png differ diff --git a/web_widget_float_formula/static/src/js/web_widget_float_formula.js b/web_widget_float_formula/static/src/js/web_widget_float_formula.js new file mode 100644 index 000000000..af88c9a61 --- /dev/null +++ b/web_widget_float_formula/static/src/js/web_widget_float_formula.js @@ -0,0 +1,200 @@ +/** + * Copyright 2014-2015 GRAP + * Copyright 2016 LasLabs Inc. + * Copyright 2020 Brainbean Apps (https://brainbeanapps.com) + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + */ +odoo.define('web_widget_float_formula', function(require) { + "use strict"; + + var field_utils = require('web.field_utils'); + var pyUtils = require('web.py_utils'); + var NumericField = require('web.basic_fields').NumericField; + var FieldMonetary = require('web.basic_fields').FieldMonetary; + + var FormulaFieldMixin = { + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Unaltered formula that user has entered. + * + * @private + */ + _formula: '', + + /** + * Value of the field that was concealed during formula reveal. + * + * @private + */ + _concealedValue: '', + + /** + * Returns formula prefix character + * + * @private + */ + _getFormulaPrefix: function () { + return '='; + }, + + /** + * Process formula if one is detected. + * + * @override + * @private + * @param {any} value + * @param {Object} [options] + */ + _setValue: function (value, options) { + this._formula = ''; + if (!!value && this._isFormula(value)) { + try { + var evaluated_value = this._evaluateFormula(value); + this._formula = value; + + value = this._formatValue(evaluated_value); + this.$input.val(value); + } catch (err) { + this._formula = ''; + } finally { + this._concealedValue = ''; + } + } + return this._super(value, options); + }, + + /** + * Checks if provided value is a formula. + * + * @private + * @param {any} value + */ + _isFormula: function(value) { + value = value.toString().replace(/\s+/gm, ''); + return value.startsWith(this._getFormulaPrefix()) + || this._getOperatorsRegExp().test(value); + }, + + /** + * Returns regular expression that matches all supported operators + * + * @private + */ + _getOperatorsRegExp: function () { + return /((?:\+)|(?:\-)|(?:\*)|(?:\/)|(?:\()|(?:\))|(?:\%))/; + }, + + /** + * Evaluate formula. + * + * @private + * @param {any} formula + */ + _evaluateFormula: function(formula) { + return pyUtils.py_eval(this._preparseFormula(formula)); + }, + + /** + * Pre-parses and sanitizes formula + * + * @private + * @param {string} formula + */ + _preparseFormula: function(formula) { + formula = formula.toString().replace(/\s+/gm, ''); + var prefix = this._getFormulaPrefix(); + if (formula.startsWith(prefix)) { + formula = formula.substring(prefix.length); + } + var operatorsRegExp = this._getOperatorsRegExp(); + return formula.split(operatorsRegExp).reduce((tokens, token) => { + if (token === '') { + return tokens; + } + if (!operatorsRegExp.test(token)) { + token = field_utils.parse.float(token); + } + tokens.push(token); + return tokens; + }, []).join(''); + }, + + /** + * Reveals formula + * + * @private + */ + _revealFormula: function () { + if (!!this._formula) { + this._concealedValue = this.$input.val(); + this.$input.val(this._formula); + } + }, + + /** + * Conceals formula + * + * @private + */ + _concealFormula: function () { + var value = this.$input.val(); + if (!!value && this._isFormula(value)) { + if (value !== this._formula) { + this.commitChanges(); + } else if (!!this._concealedValue) { + this.$input.val(this._concealedValue); + this._concealedValue = ''; + } + } + }, + + /** + * Handles 'focus' event + * + * @private + * @param {FocusEvent} event + */ + _onFocusFormulaField: function(event) { + if (this.$input === undefined || this.mode !== 'edit') { + return; + } + this._revealFormula(); + }, + + /** + * Handles 'blur' event + * + * @private + * @param {FocusEvent} event + */ + _onBlurFormulaField: function(event) { + if (this.$input === undefined || this.mode !== 'edit') { + return; + } + this._concealFormula(); + }, + }; + + NumericField.include({ + ...FormulaFieldMixin, + events: _.extend({}, NumericField.prototype.events, { + 'focus': '_onFocusFormulaField', + 'blur': '_onBlurFormulaField', + }), + }); + + FieldMonetary.include({ + ...FormulaFieldMixin, + events: _.extend({}, FieldMonetary.prototype.events, { + 'focusin': '_onFocusFormulaField', + 'focusout': '_onBlurFormulaField', + }), + }); + + return { + FormulaFieldMixin: FormulaFieldMixin, + }; +}); diff --git a/web_widget_float_formula/static/tests/js/test_web_widget_float_formula.js b/web_widget_float_formula/static/tests/js/test_web_widget_float_formula.js new file mode 100644 index 000000000..2335bd02c --- /dev/null +++ b/web_widget_float_formula/static/tests/js/test_web_widget_float_formula.js @@ -0,0 +1,169 @@ +/** + * Copyright 2016 LasLabs Inc. + * Copyright 2020 Brainbean Apps (https://brainbeanapps.com) + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + */ +odoo.define('web_widget_float_formula.test_web_widget_float_formula', function (require) { + "use strict"; + + var FormView = require('web.FormView'); + var testUtils = require('web.test_utils'); + + QUnit.module('web_widget_float_formula', {}, function () { + + QUnit.test('float field', async function (assert) { + assert.expect(5); + + const form = await testUtils.createAsyncView({ + View: FormView, + model: 'demo_entry', + data: { + demo_entry: { + fields: { + test_field: {string: 'Test Field', type: 'float'}, + }, + records: [{id: 1, test_field: 0.0}], + }, + }, + res_id: 1, + arch: + '
' + + '' + + '', + viewOptions: { + mode: 'edit', + }, + }); + + var test_field = form.$('.o_field_widget[name="test_field"]'); + + testUtils.fields.editInput(test_field, '0.0 + 40.0 + 2.0'); + assert.strictEqual(test_field.val(), '42.00'); + + test_field.triggerHandler('focus'); + assert.strictEqual(test_field.val(), '0.0 + 40.0 + 2.0'); + test_field.triggerHandler('blur'); + assert.strictEqual(test_field.val(), '42.00'); + + testUtils.fields.editInput(test_field, '=(1.5+8.0/2.0-(15+5)*0.1)'); + assert.strictEqual(test_field.val(), '3.50'); + + testUtils.fields.editInput(test_field, 'bubblegum'); + assert.strictEqual(test_field.val(), 'bubblegum'); + + form.destroy(); + }); + + QUnit.test('integer field', async function (assert) { + assert.expect(5); + + const form = await testUtils.createAsyncView({ + View: FormView, + model: 'demo_entry', + data: { + demo_entry: { + fields: { + test_field: {string: 'Test Field', type: 'integer'}, + }, + records: [{id: 1, test_field: 0}], + }, + }, + res_id: 1, + arch: + '
' + + '' + + '', + viewOptions: { + mode: 'edit', + }, + }); + + var test_field = form.$('.o_field_widget[name="test_field"]'); + + testUtils.fields.editInput(test_field, '0 + 40 + 2'); + assert.strictEqual(test_field.val(), '42'); + + test_field.triggerHandler('focus'); + assert.strictEqual(test_field.val(), '0 + 40 + 2'); + test_field.triggerHandler('blur'); + assert.strictEqual(test_field.val(), '42'); + + testUtils.fields.editInput(test_field, '=(1+8/2-(15+5)*0.1)'); + assert.strictEqual(test_field.val(), '3'); + + testUtils.fields.editInput(test_field, 'bubblegum'); + assert.strictEqual(test_field.val(), 'bubblegum'); + + form.destroy(); + }); + + QUnit.test('monetary field', async function (assert) { + assert.expect(5); + + const form = await testUtils.createAsyncView({ + View: FormView, + model: 'demo_entry', + data: { + demo_entry: { + fields: { + test_field: {string: 'Test Field', type: 'monetary'}, + currency_id: {string: 'Currency', type: 'many2one', relation: 'currency', searchable: true}, + }, + records: [{id: 1, test_field: 0.0, currency_id: 1}], + }, + currency: { + fields: { + symbol: {string: 'Currency Sumbol', type: 'char', searchable: true}, + position: {string: 'Currency Position', type: 'char', searchable: true}, + }, + records: [{ + id: 1, + display_name: '$', + symbol: '$', + position: 'before', + }] + }, + }, + res_id: 1, + arch: + '
' + + '' + + '' + + '', + viewOptions: { + mode: 'edit', + }, + session: { + currencies: { + 1: { + id: 1, + display_name: '$', + symbol: '$', + position: 'before', + }, + }, + }, + }); + + var test_field = form.$('.o_field_widget[name="test_field"]'); + var test_field_input = form.$('.o_field_widget[name="test_field"] input'); + + testUtils.fields.editInput(test_field_input, '0.0 + 40.0 + 2.0'); + assert.strictEqual(test_field_input.val(), '42.00'); + + test_field.triggerHandler('focusin'); + assert.strictEqual(test_field_input.val(), '0.0 + 40.0 + 2.0'); + test_field.triggerHandler('focusout'); + assert.strictEqual(test_field_input.val(), '42.00'); + + testUtils.fields.editInput(test_field_input, '=(1.5+8.0/2.0-(15+5)*0.1)'); + assert.strictEqual(test_field_input.val(), '3.50'); + + testUtils.fields.editInput(test_field_input, 'bubblegum'); + assert.strictEqual(test_field_input.val(), 'bubblegum'); + + form.destroy(); + }); + + }); +}); diff --git a/web_widget_float_formula/templates/assets.xml b/web_widget_float_formula/templates/assets.xml new file mode 100644 index 000000000..584f61446 --- /dev/null +++ b/web_widget_float_formula/templates/assets.xml @@ -0,0 +1,20 @@ + + + +