diff --git a/web_widget_float_formula/README.rst b/web_widget_float_formula/README.rst index 83b7c132..092e8aed 100644 --- a/web_widget_float_formula/README.rst +++ b/web_widget_float_formula/README.rst @@ -1,36 +1,61 @@ -Allow to write simple mathematic formulas in Integer / Float fields -=================================================================== +.. 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 -* Possibility to tip a text like "=45 + 4/3 - 5 * (2 +1)"; -* if the formula is correct, The result will be computed and displayed; -* if the formula is not correct, the initial text is displayed; +======================== +Formulas in Float Fields +======================== -Technical informations ----------------------- +This module allows the use of simple math formulas in integer/float fields +(e.g. "=45 + 4/3 - 5 * (2 + 1)"). -* Overloads "instance.web.form.FieldFloat"; (so works for fields.integer & - fields.float); -* To compute, the module simply use the eval() javascript function; -* Rounding computation is not done by this module (The module has the same - behaviour if the user tips "=1/3" or if he tips "0.33[...]"); -* avoid code injonction by regexpr test: "=alert('security')" is not valid; +* 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 ===== -See demo here Video: http://www.youtube.com/watch?v=jQGdD34WYrA&hd=1 +Install and enjoy. A short demo video can be found at +http://www.youtube.com/watch?v=jQGdD34WYrA. -Roadmap / Limit -=============== -* Only supports the four operators: "+" "-" "*" "/" and parenthesis; +.. 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/9.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 smashing it by providing a detailed and welcomed feedback -`here `_. +If you spotted it first, help us smash it by providing detailed and welcomed +feedback. Credits ======= @@ -39,17 +64,19 @@ 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 +.. 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. +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 41a41f91..e69de29b 100644 --- a/web_widget_float_formula/__init__.py +++ b/web_widget_float_formula/__init__.py @@ -1,4 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################### -# See __openerp__.py file for Copyright and Licence Informations. -############################################################################### diff --git a/web_widget_float_formula/__openerp__.py b/web_widget_float_formula/__openerp__.py index 4710a59c..3b901505 100644 --- a/web_widget_float_formula/__openerp__.py +++ b/web_widget_float_formula/__openerp__.py @@ -1,19 +1,22 @@ -# -*- encoding: utf-8 -*- -############################################################################### -# See Copyright and Licence Informations undermentioned. -############################################################################### +# -*- coding: utf-8 -*- +# Copyright GRAP +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + { - 'name': 'Web Widget - Formulas in Float fields', - 'version': '8.0.1.0.0', - 'category': 'web', - 'author': 'GRAP,Odoo Community Association (OCA)', + 'name': 'Web Widget - Formulas in Float Fields', + 'summary': 'Allow use of simple formulas in float fields', + 'version': '9.0.1.0.0', + 'category': 'Web', + 'author': 'GRAP, LasLabs, Odoo Community Association (OCA)', 'website': 'http://www.grap.coop', 'license': 'AGPL-3', 'depends': [ 'web', ], 'data': [ - 'views/qweb.xml', + 'views/web_widget_float_formula.xml', ], - 'installable': False, + 'installable': True, + 'application': False, } diff --git a/web_widget_float_formula/static/src/js/models.js b/web_widget_float_formula/static/src/js/models.js deleted file mode 100644 index 6a132251..00000000 --- a/web_widget_float_formula/static/src/js/models.js +++ /dev/null @@ -1,132 +0,0 @@ -/******************************************************************************* -See __openerp__.py file for Copyright and Licence Informations. -*******************************************************************************/ - -openerp.web_widget_float_formula = function (instance) { - - instance.web.FormView = instance.web.FormView.extend({ - /*********************************************************************** - Overload section - ***********************************************************************/ - - /** - * Overload : '_process_save' function - 1: to force computation of formula if the user realize a keydown directly after the formula input in a tree view ; - 2: to clean up the '_formula_text' value in all case 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')){ - currentval = f.$('input').attr('value') - if (typeof currentval != 'undefined'){ - formula = f._get_valid_expression(currentval); - if (formula){ - f._compute_result(); - } - } - f._clean_formula_text(); - } - } - return this._super(save_obj); - }, - - }); - - instance.web.form.FieldFloat = instance.web.form.FieldFloat.extend({ - /*********************************************************************** - Overload section - ***********************************************************************/ - - /** - * Overload : 'start' function to catch 'blur' and 'focus' events. - */ - start: function() { - this.on("blurred", this, this._compute_result); - this.on("focused", this, this._display_formula); - return this._super(); - }, - - /** - * Overload : 'initialize_content' function to clean '_formula_text' value. - */ - initialize_content: function() { - this._clean_formula_text(); - return this._super(); - }, - - /*********************************************************************** - Custom section - ***********************************************************************/ - - /** - * keep in memory the formula to allow user to edit it again. - The formula has to be keeped in memory until a 'save' action. - */ - _formula_text: '', - - /** - * Clean '_formula_text' value. - */ - _clean_formula_text: function() { - this._formula_text = ''; - }, - - /** - * Return a valid formula from a val, if possible. - Otherwise, return false. - */ - _get_valid_expression: function(val) { - // Trim the value - currenttxt = val.toString().replace(/^\s+|\s+$/g, ''); - // Test if the value is a formula - if (currenttxt[0] == '=') { - // allowed chars : [0-9] .,+-/*() and spaces - myreg = RegExp('[0-9]|\\s|\\.|,|\\(|\\)|\\+|\\-|\\*|\\/','g') - // Test to avoid code injonction in eval function. - if (currenttxt.substring(1).replace(myreg, '') == ''){ - try { - // Try to compute - formula = currenttxt.substring(1).replace(/,/g,'.'); - var floatval = eval(formula); - }catch (e) {} - if (typeof (floatval) != 'undefined'){ - return formula; - } - } - } - return false; - }, - - /** - * test if the content of the field is a valid formula, - * compute the result, and replace the current value by the final result. - */ - _compute_result: function() { - var formula - // Erase old formula - this._formula_text = ''; - - formula = this._get_valid_expression(this.$el.find('input').attr('value')); - if (formula){ - // Store new formula - this._formula_text = "=" + formula; - // put the result in the field - this.set_value(eval(formula)); - // Force rendering anyway to avoid format loss if no change - this.render_value(); - } - }, - - /** - * Display the stored formula in the field, to allow modification. - */ - _display_formula: function() { - if (this._formula_text != ''){ - this.$el.find('input').val(this._formula_text); - } - }, - - }); -}; 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 00000000..a64617f2 --- /dev/null +++ b/web_widget_float_formula/static/src/js/web_widget_float_formula.js @@ -0,0 +1,103 @@ +/** +* Copyright GRAP +* Copyright 2016 LasLabs Inc. +* License AGPL-3.0 or later (http://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._compute_result(); + f._clean_formula_text(); + } + } + + return this._super(save_obj); + }, + }); + + 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.on('blurred', this, this._compute_result); + this.on('focused', this, this._display_formula); + return this._super(); + }, + + initialize_content: function() { + this._clean_formula_text(); + return this._super(); + }, + + _formula_text: '', + + _clean_formula_text: function() { + this._formula_text = ''; + }, + + _process_formula: function(formula) { + var clean_formula = formula.toString().replace(/^\s+|\s+$/g, ''); + if (clean_formula[0] == '=') { + clean_formula = clean_formula.substring(1); + var myreg = 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 formula = this._process_formula(this.$el.find('input').val()); + 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.$el.find('input').val(this._formula_text); + } + }, + }); + }); +}); 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 00000000..808dd56e --- /dev/null +++ b/web_widget_float_formula/static/tests/js/test_web_widget_float_formula.js @@ -0,0 +1,161 @@ +/** +* Copyright 2016 LasLabs Inc. +* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +**/ + +odoo.define_section('web_widget_float_formula', ['web.form_common', 'web.form_widgets', 'web.core'], function(test) { + 'use strict'; + + 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.$element = $(''); + self.field.$el.append(self.$element); + }; + + test('Float fields should have a _formula_text property that defaults to an empty string', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + + assert.strictEqual(this.field._formula_text, ''); + }); + + test('.initialize_content() on float fields should clear the _formula_text property', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + this.field._formula_text = 'test'; + this.field.initialize_content(); + + assert.strictEqual(this.field._formula_text, ''); + }); + + test('._clean_formula_text() on float fields should clear the _formula_text property', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + this.field._formula_text = 'test'; + this.field._clean_formula_text(); + + assert.strictEqual(this.field._formula_text, ''); + }); + + test('._process_formula() on float fields should return false when given invalid formulas', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + + assert.strictEqual(this.field._process_formula('2*3'), false); + assert.strictEqual(this.field._process_formula('=2*3a'), false); + }); + + test('._process_formula() on float fields should properly process a valid formula', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + + assert.strictEqual(this.field._process_formula(' =2*3\n'), '2*3'); + }); + + test('._eval_formula() on float fields should properly evaluate a valid formula', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + + assert.equal(this.field._eval_formula('2*3'), 6); + }); + + test('._eval_formula() on float fields should properly handle alternative decimal points and thousands seps', + function(assert, form_common, form_widgets, core) { + var translation_params = core._t.database.parameters; + translation_params.decimal_point = ','; + translation_params.thousands_sep = '.'; + window.test_setup(this, form_common, form_widgets, core); + + assert.equal(this.field._eval_formula('2.000*3,5'), 7000); + }); + + test('._eval_formula() on float fields should return false when given an input that evals to undefined', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + + assert.equal(this.field._eval_formula(''), false); + }); + + test('._eval_formula() on float fields should return false when given an input that cannot be evaluated', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + + assert.equal(this.field._eval_formula('*/'), false); + }); + + test('._compute_result() on float fields should always clean up _formula_text', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + this.field._formula_text = 'test'; + this.field._compute_result(); + + assert.strictEqual(this.field._formula_text, ''); + }); + + test('._compute_result() should not change the value of the associated input when it is not a valid formula', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + this.$element.val('=2*3a'); + this.field._compute_result(); + + assert.strictEqual(this.$element.val(), '=2*3a'); + }); + + test('._compute_result() should not change the value of the associated input when it cannot be evaled', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + this.$element.val('=*/'); + this.field._compute_result(); + + assert.strictEqual(this.$element.val(), '=*/'); + }); + + test('._compute_result() should behave properly when the current value of the input element is a valid formula', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + this.$element.val('=2*3'); + this.field._compute_result(); + + assert.equal(this.$element.val(), '6'); + assert.strictEqual(this.field._formula_text, '=2*3'); + }); + + test('._display_formula() should update the value of the input element when there is a stored formula', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + this.field._formula_text = "test"; + this.field._display_formula(); + + assert.equal(this.$element.val(), 'test'); + }); + + test('.start() on float fields should add a handler that calls ._compute_result() when the field is blurred', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + this.field.called = false; + this.field._compute_result = function() { + this.called = true; + }; + this.field.start(); + this.field.trigger('blurred'); + + assert.strictEqual(this.field.called, true); + }); + + test('.start() on float fields should add a handler that calls ._display_formula() when the field is focused', + function(assert, form_common, form_widgets, core) { + window.test_setup(this, form_common, form_widgets, core); + this.field.called = false; + this.field._display_formula = function() { + this.called = true; + }; + this.field.start(); + this.field.trigger('focused'); + + assert.strictEqual(this.field.called, true); + }); + +}); diff --git a/web_widget_float_formula/tests/__init__.py b/web_widget_float_formula/tests/__init__.py new file mode 100644 index 00000000..9cf9e2d2 --- /dev/null +++ b/web_widget_float_formula/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import test_js diff --git a/web_widget_float_formula/tests/test_js.py b/web_widget_float_formula/tests/test_js.py new file mode 100644 index 00000000..4e6b155e --- /dev/null +++ b/web_widget_float_formula/tests/test_js.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from openerp.tests import HttpCase + + +class TestJS(HttpCase): + + def test_js(self): + self.phantom_js( + "/web/tests?module=web_widget_float_formula", + "", + login="admin", + ) diff --git a/web_widget_float_formula/views/qweb.xml b/web_widget_float_formula/views/qweb.xml deleted file mode 100644 index b7467559..00000000 --- a/web_widget_float_formula/views/qweb.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/web_widget_float_formula/views/web_widget_float_formula.xml b/web_widget_float_formula/views/web_widget_float_formula.xml new file mode 100644 index 00000000..63857f77 --- /dev/null +++ b/web_widget_float_formula/views/web_widget_float_formula.xml @@ -0,0 +1,21 @@ + + + + + +