3
0
Fork 0

[MIG] web_widget_float_formula: Migration to 12.0

12.0
Alexey Pelykh 2020-04-25 11:35:49 +02:00
parent 6f772e19be
commit 61691cc090
11 changed files with 378 additions and 369 deletions

View File

@ -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 <https://github.com/OCA/web/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 <o.bulkin@gmail.com>
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.

View File

@ -0,0 +1 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

View File

@ -1,22 +1,22 @@
# -*- coding: utf-8 -*- # Copyright 2014-2015 GRAP
# Copyright GRAP
# Copyright 2016 LasLabs Inc. # 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', 'name': 'Web Widget - Formulas in Float Fields',
'summary': 'Allow use of simple 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', 'category': 'Web',
'author': 'GRAP, LasLabs, Odoo Community Association (OCA)', 'author':
'website': 'http://www.grap.coop', 'GRAP, LasLabs, Brainbean Apps, Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/web/',
'license': 'AGPL-3', 'license': 'AGPL-3',
'depends': [ 'depends': [
'web', 'web',
], ],
'data': [ 'data': [
'views/web_widget_float_formula.xml', 'templates/assets.xml',
], ],
'installable': True, 'installable': True,
'application': False,
} }

View File

@ -0,0 +1,3 @@
* Sylvain Le Gal (https://twitter.com/legalsylvain)
* Oleg Bulkin <o.bulkin@gmail.com>
* Alexey Pelykh <alexey.pelykh@brainbeanapps.com>

View File

@ -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

View File

@ -0,0 +1 @@
This module is not needed for v13, as this feature is bundled with Odoo v13.

View File

@ -1,111 +1,200 @@
/** /**
* Copyright GRAP * Copyright 2014-2015 GRAP
* Copyright 2016 LasLabs Inc. * 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).
*/
odoo.define('web_widget_float_formula', function(require) { odoo.define('web_widget_float_formula', function(require) {
"use strict"; "use strict";
var form_view = require('web.FormView'); var field_utils = require('web.field_utils');
form_view.include({ var pyUtils = require('web.py_utils');
// Ensure that formula is computed even if user saves right away and var NumericField = require('web.basic_fields').NumericField;
// clean up '_formula_text' value to avoid bugs in tree view var FieldMonetary = require('web.basic_fields').FieldMonetary;
_process_save: function(save_obj) {
for (var f in this.fields) { var FormulaFieldMixin = {
if (!this.fields.hasOwnProperty(f)) { continue; } //--------------------------------------------------------------------------
f = this.fields[f]; // Private
if (f.hasOwnProperty('_formula_text') && f.$el.find('input').length > 0) { //--------------------------------------------------------------------------
f._compute_result();
f._clean_formula_text(); /**
* 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);
return this._super(save_obj);
}, },
/**
* 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'); FieldMonetary.include({
core.bus.on('web_client_ready', null, function () { ...FormulaFieldMixin,
// Import localization values used to eval formula events: _.extend({}, FieldMonetary.prototype.events, {
var translation_params = core._t.database.parameters; 'focusin': '_onFocusFormulaField',
var decimal_point = translation_params.decimal_point; 'focusout': '_onBlurFormulaField',
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);
}
},
});
}); });
return {
FormulaFieldMixin: FormulaFieldMixin,
};
}); });

View File

@ -1,161 +1,169 @@
/** /**
* Copyright 2016 LasLabs Inc. * 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).
*/
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) { var FormView = require('web.FormView');
'use strict'; var testUtils = require('web.test_utils');
window.test_setup = function(self, form_common, form_widgets, core) { QUnit.module('web_widget_float_formula', {}, function () {
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 = $('<input>');
self.field.$label = $('<label>');
};
test('Float fields should have a _formula_text property that defaults to an empty string', QUnit.test('float field', async function (assert) {
function(assert, form_common, form_widgets, core) { assert.expect(5);
window.test_setup(this, form_common, form_widgets, core);
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:
'<form>' +
'<field name="test_field"/>' +
'</form>',
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:
'<form>' +
'<field name="test_field"/>' +
'</form>',
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:
'<form>' +
'<field name="test_field" widget="monetary"/>' +
'<field name="currency_id" invisible="1"/>' +
'</form>',
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();
});
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.field.$input.val('=2*3a');
this.field._compute_result();
assert.strictEqual(this.field.$input.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.field.$input.val('=*/');
this.field._compute_result();
assert.strictEqual(this.field.$input.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.field.$input.val('=2*3');
this.field._compute_result();
assert.equal(this.field.$input.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.field.$input.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);
});
}); });

View File

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright GRAP Copyright 2014-2015 GRAP
Copyright 2016 LasLabs Inc. 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).
--> -->
<odoo> <odoo>
<template id="assets_backend" name="web_widget_float_formula Assets" inherit_id="web.assets_backend"> <template id="assets_backend" name="web_widget_float_formula Assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside"> <xpath expr="." position="inside">

View File

@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_js

View File

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import odoo.tests
class TestJS(odoo.tests.HttpCase):
def test_js(self):
self.phantom_js(
"/web/tests?module=web_widget_float_formula",
"console.log('ok')",
"",
login="admin"
)