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 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,
}

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.
* 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) {
"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;
return this._super(save_obj);
},
});
var FormulaFieldMixin = {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
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;
/**
* Unaltered formula that user has entered.
*
* @private
*/
_formula: '',
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;
/**
* Value of the field that was concealed during formula reveal.
*
* @private
*/
_concealedValue: '',
/**
* Returns formula prefix character
*
* @private
*/
_getFormulaPrefix: function () {
return '=';
},
initialize_content: function() {
this._clean_formula_text();
return this._super();
},
_formula_text: '',
_clean_formula_text: function() {
this._formula_text = '';
},
_process_formula: function(formula) {
/**
* 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 {
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;
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 false;
return this._super(value, options);
},
_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;
/**
* 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);
},
_compute_result: function() {
this._clean_formula_text();
/**
* Returns regular expression that matches all supported operators
*
* @private
*/
_getOperatorsRegExp: function () {
return /((?:\+)|(?:\-)|(?:\*)|(?:\/)|(?:\()|(?:\))|(?:\%))/;
},
var input = this.$input.val();
/**
* Evaluate formula.
*
* @private
* @param {any} formula
*/
_evaluateFormula: function(formula) {
return pyUtils.py_eval(this._preparseFormula(formula));
},
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();
/**
* 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 = '';
}
}
},
// Display the formula stored in the field to allow modification
_display_formula: function() {
if (this._formula_text !== '') {
this.$input.val(this._formula_text);
/**
* 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,
};
});

View File

@ -1,161 +1,169 @@
/**
* 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) {
'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 = $('<input>');
self.field.$label = $('<label>');
};
QUnit.module('web_widget_float_formula', {}, function () {
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);
QUnit.test('float field', async function (assert) {
assert.expect(5);
assert.strictEqual(this.field._formula_text, '');
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',
},
});
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();
var test_field = form.$('.o_field_widget[name="test_field"]');
assert.strictEqual(this.field._formula_text, '');
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();
});
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();
QUnit.test('integer field', async function (assert) {
assert.expect(5);
assert.strictEqual(this.field._formula_text, '');
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',
},
});
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);
var test_field = form.$('.o_field_widget[name="test_field"]');
assert.strictEqual(this.field._process_formula('2*3'), false);
assert.strictEqual(this.field._process_formula('=2*3a'), false);
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();
});
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);
QUnit.test('monetary field', async function (assert) {
assert.expect(5);
assert.strictEqual(this.field._process_formula(' =2*3\n'), '2*3');
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',
},
},
},
});
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);
var test_field = form.$('.o_field_widget[name="test_field"]');
var test_field_input = form.$('.o_field_widget[name="test_field"] input');
assert.equal(this.field._eval_formula('2*3'), 6);
});
testUtils.fields.editInput(test_field_input, '0.0 + 40.0 + 2.0');
assert.strictEqual(test_field_input.val(), '42.00');
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);
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');
assert.equal(this.field._eval_formula('2.000*3,5'), 7000);
});
testUtils.fields.editInput(test_field_input, '=(1.5+8.0/2.0-(15+5)*0.1)');
assert.strictEqual(test_field_input.val(), '3.50');
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);
testUtils.fields.editInput(test_field_input, 'bubblegum');
assert.strictEqual(test_field_input.val(), 'bubblegum');
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);
form.destroy();
});
});
});

View File

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="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).
-->
<odoo>
<template id="assets_backend" name="web_widget_float_formula Assets" inherit_id="web.assets_backend">
<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"
)