From 61691cc0900d2849e6622f14c966b5ef2bc23723 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Sat, 25 Apr 2020 11:35:49 +0200 Subject: [PATCH] [MIG] web_widget_float_formula: Migration to 12.0 --- web_widget_float_formula/README.rst | 82 ----- web_widget_float_formula/__init__.py | 1 + web_widget_float_formula/__manifest__.py | 16 +- .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 11 + web_widget_float_formula/readme/ROADMAP.rst | 1 + .../static/src/js/web_widget_float_formula.js | 289 ++++++++++------ .../tests/js/test_web_widget_float_formula.js | 316 +++++++++--------- .../assets.xml} | 7 +- web_widget_float_formula/tests/__init__.py | 5 - web_widget_float_formula/tests/test_js.py | 16 - 11 files changed, 378 insertions(+), 369 deletions(-) delete mode 100644 web_widget_float_formula/README.rst create mode 100644 web_widget_float_formula/readme/CONTRIBUTORS.rst create mode 100644 web_widget_float_formula/readme/DESCRIPTION.rst create mode 100644 web_widget_float_formula/readme/ROADMAP.rst rename web_widget_float_formula/{views/web_widget_float_formula.xml => templates/assets.xml} (81%) delete mode 100644 web_widget_float_formula/tests/__init__.py delete mode 100644 web_widget_float_formula/tests/test_js.py diff --git a/web_widget_float_formula/README.rst b/web_widget_float_formula/README.rst deleted file mode 100644 index 4804167ea..000000000 --- a/web_widget_float_formula/README.rst +++ /dev/null @@ -1,82 +0,0 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 - -======================== -Formulas in Float Fields -======================== - -This module allows the use of simple math formulas in integer/float fields -(e.g. "=45 + 4/3 - 5 * (2 + 1)"). - -* Only supports parentheses, decimal points, thousands separators, and the - operators "+", "-", "*", and "/" -* Will use the decimal point and thousands separator characters associated - with your language -* If the formula is valid, the result will be computed and displayed, and the - formula will be stored for editing -* If the formula is not valid, it's retained in the field as text - -**Technical Details** - -* Overloads web.form_widgets.FieldFloat (so it works for fields.integer & - fields.float) -* Uses the eval() JS function to evaluate the formula -* Does not do any rounding (this is handled elsewhere) -* Avoids code injection by applying strict regex to formula prior to eval() - (e.g. "=alert('security')" would not get evaluated) - -Installation -============ - -To install this module, simply follow the standard install process. - -Configuration -============= - -No configuration is needed or possible. - -Usage -===== - -Install and enjoy. A short demo video can be found at -http://www.youtube.com/watch?v=jQGdD34WYrA. - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/162/10.0 - -Known Issues / Roadmap -====================== - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues `_. -In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smash it by providing detailed and welcomed -feedback. - -Credits -======= - -Contributors ------------- - -* Sylvain Le Gal (https://twitter.com/legalsylvain) -* Oleg Bulkin - -Maintainer ----------- - -.. image:: http://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: http://odoo-community.org - -This module is maintained by the OCA. - -OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use. - -To contribute to this module, please visit http://odoo-community.org. diff --git a/web_widget_float_formula/__init__.py b/web_widget_float_formula/__init__.py index e69de29bb..c71289ab1 100644 --- a/web_widget_float_formula/__init__.py +++ 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 index 888b1be3d..c56adb15f 100644 --- a/web_widget_float_formula/__manifest__.py +++ b/web_widget_float_formula/__manifest__.py @@ -1,22 +1,22 @@ -# -*- coding: utf-8 -*- -# Copyright GRAP +# Copyright 2014-2015 GRAP # Copyright 2016 LasLabs Inc. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# 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': '10.0.1.0.0', + 'version': '12.0.1.0.0', 'category': 'Web', - 'author': 'GRAP, LasLabs, Odoo Community Association (OCA)', - 'website': 'http://www.grap.coop', + 'author': + 'GRAP, LasLabs, Brainbean Apps, Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/web/', 'license': 'AGPL-3', 'depends': [ 'web', ], 'data': [ - 'views/web_widget_float_formula.xml', + 'templates/assets.xml', ], 'installable': True, - 'application': False, } 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/src/js/web_widget_float_formula.js b/web_widget_float_formula/static/src/js/web_widget_float_formula.js index 065d5acf2..af88c9a61 100644 --- 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 @@ -1,111 +1,200 @@ /** -* Copyright GRAP -* Copyright 2016 LasLabs Inc. -* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -**/ - + * 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 form_view = require('web.FormView'); - form_view.include({ - // Ensure that formula is computed even if user saves right away and - // clean up '_formula_text' value to avoid bugs in tree view - _process_save: function(save_obj) { - for (var f in this.fields) { - if (!this.fields.hasOwnProperty(f)) { continue; } - f = this.fields[f]; - if (f.hasOwnProperty('_formula_text') && f.$el.find('input').length > 0) { - f._compute_result(); - f._clean_formula_text(); + 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(save_obj); + 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', + }), }); - var core = require('web.core'); - core.bus.on('web_client_ready', null, function () { - // Import localization values used to eval formula - var translation_params = core._t.database.parameters; - var decimal_point = translation_params.decimal_point; - var thousands_sep = translation_params.thousands_sep; - - var field_float = require('web.form_widgets').FieldFloat; - field_float.include({ - start: function() { - this._super(); - this.on('blurred', this, this._compute_result); - this.on('focused', this, this._display_formula); - return this; - }, - - initialize_content: function() { - this._clean_formula_text(); - return this._super(); - }, - - _formula_text: '', - - _clean_formula_text: function() { - this._formula_text = ''; - }, - - _process_formula: function(formula) { - try{ - formula = formula.toString(); - } catch (ex) { - return false; - } - var clean_formula = formula.toString().replace(/^\s+|\s+$/g, ''); - if (clean_formula[0] == '=') { - clean_formula = clean_formula.substring(1); - var myreg = new RegExp('[0-9]|\\s|\\.|,|\\(|\\)|\\+|\\-|\\*|\\/', 'g'); - if (clean_formula.replace(myreg, '') === '') { - return clean_formula; - } - } - return false; - }, - - _eval_formula: function(formula) { - var value; - formula = formula.replace(thousands_sep, '').replace(decimal_point, '.'); - try { - value = eval(formula); - } - catch(e) {} - - if (typeof value != 'undefined') { - return value; - } - return false; - }, - - _compute_result: function() { - this._clean_formula_text(); - - var input = this.$input.val(); - - var formula = this._process_formula(input); - if (formula !== false) { - var value = this._eval_formula(formula); - if (value !== false) { - this._formula_text = "=" + formula; - this.set_value(value); - // Force rendering to avoid format loss if there's no change - this.render_value(); - } - } - }, - - // Display the formula stored in the field to allow modification - _display_formula: function() { - if (this._formula_text !== '') { - this.$input.val(this._formula_text); - } - }, - }); + 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 index 1d7b194da..2335bd02c 100644 --- 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 @@ -1,161 +1,169 @@ /** -* Copyright 2016 LasLabs Inc. -* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -**/ + * 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"; -odoo.define_section('web_widget_float_formula', ['web.form_common', 'web.form_widgets', 'web.core'], function(test) { - 'use strict'; + var FormView = require('web.FormView'); + var testUtils = require('web.test_utils'); - window.test_setup = function(self, form_common, form_widgets, core) { - core.bus.trigger('web_client_ready'); - var field_manager = new form_common.DefaultFieldManager(null, {}); - var filler = {'attrs': {}}; // Needed to instantiate FieldFloat - self.field = new form_widgets.FieldFloat(field_manager, filler); - self.field.$input = $(''); - self.field.$label = $('