diff --git a/addons/uom/models/uom_uom.py b/addons/uom/models/uom_uom.py
index 151ae10aa47..41c2271da0d 100644
--- a/addons/uom/models/uom_uom.py
+++ b/addons/uom/models/uom_uom.py
@@ -22,6 +22,12 @@ class UoMCategory(models.Model):
('uom_category_unique_type', 'UNIQUE(measure_type)', 'You can have only one category per measurement type.'),
]
+ @api.multi
+ def unlink(self):
+ if self.filtered(lambda categ: categ.measure_type == 'time'):
+ raise UserError(_("You cannot delete this UoM Category as it is used by the system."))
+ return super(UoMCategory, self).unlink()
+
class UoM(models.Model):
_name = 'uom.uom'
@@ -102,6 +108,12 @@ class UoM(models.Model):
values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0
return super(UoM, self).write(values)
+ @api.multi
+ def unlink(self):
+ if self.filtered(lambda uom: uom.measure_type == 'time'):
+ raise UserError(_("You cannot delete this UoM as it is used by the system. You should rather archive it."))
+ return super(UoM, self).unlink()
+
@api.model
def name_create(self, name):
""" The UoM category and factor are required, so we'll have to add temporary values
diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js
index 3068a6528c9..f4031fccec9 100644
--- a/addons/web/static/src/js/core/utils.js
+++ b/addons/web/static/src/js/core/utils.js
@@ -511,6 +511,26 @@ var utils = {
return Object.freeze(obj);
},
+ /**
+ * Find the closest value of the given one in the provided array
+ *
+ * @param {Number} num
+ * @param {Array} arr
+ * @returns {Number|undefined}
+ */
+ closestNumber: function (num, arr) {
+ var curr = arr[0];
+ var diff = Math.abs (num - curr);
+ for (var val = 0; val < arr.length; val++) {
+ var newdiff = Math.abs (num - arr[val]);
+ if (newdiff < diff) {
+ diff = newdiff;
+ curr = arr[val];
+ }
+ }
+ return curr;
+ },
+
};
return utils;
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index f209925f37c..399647d1dbe 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -898,6 +898,142 @@ var FieldFloatTime = FieldFloat.extend({
formatType: 'float_time',
});
+var FieldFloatFactor = FieldFloat.extend({
+ supportedFieldTypes: ['float'],
+ className: 'o_field_float_factor',
+ formatType: 'float_factor',
+
+ /**
+ * @constructor
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ // default values
+ if (!this.nodeOptions.factor){
+ this.nodeOptions.factor = 1;
+ }
+ // use as format and parse options
+ this.parseOptions = this.nodeOptions;
+ }
+});
+
+/**
+ * The goal of this widget is to replace the input field by a button containing a
+ * range of possible values (given in the options). Each click allows the user to loop
+ * in the range. The purpose here is to restrict the field value to a predefined selection.
+ * Also, the widget support the factor conversion as the *float_factor* widget (Range values
+ * should be the result of the conversion).
+ **/
+var FieldFloatToggle = AbstractField.extend({
+ supportedFieldTypes: ['float'],
+ formatType: 'float_factor',
+ className: 'o_field_float_toggle',
+ tagName: 'span',
+ events: {
+ click: '_onClick'
+ },
+
+ /**
+ * @constructor
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+
+ this.formatType = 'float_factor';
+
+ if (this.mode === 'edit') {
+ this.tagName = 'button';
+ }
+
+ // we don't inherit Float Field
+ if (this.attrs.digits) {
+ this.nodeOptions.digits = JSON.parse(this.attrs.digits);
+ }
+ // default values
+ if (!this.nodeOptions.factor){
+ this.nodeOptions.factor = 1;
+ }
+ if (!this.nodeOptions.range){
+ this.nodeOptions.range = [0.0, 0.5, 1.0];
+ }
+
+ // use as format and parse options
+ this.parseOptions = this.nodeOptions;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Get the display value but in real type to use it in calculations
+ *
+ * @private
+ * @returns {float} The current formatted value
+ */
+ _getDisplayedValue: function () {
+ return parseFloat(this._formatValue(this.value));
+ },
+ /**
+ * Formats the HTML input tag for edit mode and stores selection status.
+ *
+ * @override
+ * @private
+ */
+ _renderEdit: function () {
+ // Keep a reference to the input so $el can become something else
+ // without losing track of the actual input.
+ this.$el.text(this._formatValue(this.value));
+ },
+ /**
+ * Resets the content to the formated value in readonly mode.
+ *
+ * @override
+ * @private
+ */
+ _renderReadonly: function () {
+ this.$el.text(this._formatValue(this.value));
+ },
+ /**
+ * Get the next value in the range, from the current one. If the current
+ * one is not in the range, the next value of the closest one will be chosen.
+ *
+ * @private
+ * @returns {number} The next formatted value in the range
+ */
+ _nextValue: function () {
+ var range = this.nodeOptions.range;
+ var val = utils.closestNumber(this._getDisplayedValue(), range);
+ var index = _.indexOf(range, val);
+ if (index !== -1) {
+ if (index + 1 < range.length) {
+ return range[index + 1];
+ }
+ }
+ return range[0];
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Clicking on the button triggers the change of value; the next one of
+ * the range will be displayed.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onClick: function(ev) {
+ if (this.mode === 'edit') {
+ ev.stopPropagation(); // only stop propagation in edit mode
+ var next_val = this._nextValue();
+ this._setValue(next_val.toString()); // will be parsed in _setValue
+ }
+ },
+
+});
+
var FieldPercentage = FieldFloat.extend({
formatType:'percentage',
});
@@ -1102,7 +1238,7 @@ var CopyClipboard = {
$clipboardBtn.tooltip({title: _t('Copied !'), trigger: 'manual', placement: 'right'});
this.clipboard = new ClipboardJS($clipboardBtn[0], {
text: function (_) {
- return self.value.trim();
+ return self.value.trim();
},
// Container added because of Bootstrap modal that give the focus to another element.
// We need to give to correct focus to ClipboardJS (see in ClipboardJS doc)
@@ -2757,6 +2893,8 @@ return {
FieldDomain: FieldDomain,
FieldFloat: FieldFloat,
FieldFloatTime: FieldFloatTime,
+ FieldFloatFactor: FieldFloatFactor,
+ FieldFloatToggle: FieldFloatToggle,
FieldPercentage : FieldPercentage,
FieldInteger: FieldInteger,
FieldMonetary: FieldMonetary,
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 20af2a01064..df8ffc9dd5b 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -53,6 +53,8 @@ registry
.add('statinfo', basic_fields.StatInfo)
.add('percentpie', basic_fields.FieldPercentPie)
.add('float_time', basic_fields.FieldFloatTime)
+ .add('float_factor', basic_fields.FieldFloatFactor)
+ .add('float_toggle', basic_fields.FieldFloatToggle)
.add('progressbar', basic_fields.FieldProgressBar)
.add('toggle_button', basic_fields.FieldToggleBoolean)
.add('dashboard_graph', basic_fields.JournalDashboardGraph)
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 989f99c5fef..0e9dc063834 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -173,6 +173,21 @@ function formatFloat(value, field, options) {
return formatted.join(l10n.decimal_point);
}
+
+/**
+ * Returns a string representing a float value, from a float converted with a
+ * factor.
+ *
+ * @param {number} value
+ * @param {number} [options.factor]
+ * Conversion factor, default value is 1.0
+ * @returns {string}
+ */
+function formatFloatFactor(value, field, options) {
+ var factor = options.factor || 1;
+ return formatFloat(value * factor, field, options);
+}
+
/**
* Returns a string representing a time value, from a float. The idea is that
* we sometimes want to display something like 1:45 instead of 1.75, or 0:15
@@ -528,6 +543,18 @@ function parseMonetary(value, field, options) {
return parseFloat(values[0] === currency.symbol ? values[1] : values[0]);
}
+/**
+ * Parse a String containing float and unconvert it with a conversion factor
+ *
+ * @param {number} [options.factor]
+ * Conversion factor, default value is 1.0
+ */
+function parseFloatFactor(value, field, options) {
+ var parsed = parseFloat(value);
+ var factor = options.factor || 1.0;
+ return parsed / factor;
+}
+
function parseFloatTime(value) {
var factor = 1;
if (value[0] === '-') {
@@ -596,6 +623,7 @@ return {
date: formatDate,
datetime: formatDateTime,
float: formatFloat,
+ float_factor: formatFloatFactor,
float_time: formatFloatTime,
html: _.identity, // todo
integer: formatInteger,
@@ -615,6 +643,7 @@ return {
date: parseDate, // todo
datetime: parseDateTime, // todo
float: parseFloat,
+ float_factor: parseFloatFactor,
float_time: parseFloatTime,
html: _.identity, // todo
integer: parseInteger,
diff --git a/addons/web/static/src/scss/fields.scss b/addons/web/static/src/scss/fields.scss
index 5f14493c613..9a1e904fdc2 100644
--- a/addons/web/static/src/scss/fields.scss
+++ b/addons/web/static/src/scss/fields.scss
@@ -429,3 +429,8 @@
}
}
+
+button.o_field_float_toggle {
+ width: 100%;
+ text-align: center;
+}
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 030d173dbeb..1855b032bf3 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -3264,6 +3264,90 @@ QUnit.module('basic_fields', {
});
+ QUnit.module('FieldFloatFactor');
+
+ QUnit.test('float_factor field in form view', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ // 16.4 / 2 = 8.2
+ assert.strictEqual(args.args[1].qux, 4.6, 'the correct float value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 5,
+ });
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '4.55', // 9.1 / 0.5
+ 'The formatted value should be displayed properly.');
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input').val(), '4.55',
+ 'The value should be rendered correctly in the input.');
+
+ form.$('input').val('2.3').trigger('input');
+
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '2.30',
+ 'The new value should be saved and displayed properly.');
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldFloatToggle');
+
+ QUnit.test('float_toggle field in form view', function (assert) {
+ assert.expect(5);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ // 1.000 / 0.125 = 8
+ assert.strictEqual(args.args[1].qux, 8, 'the correct float value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '0.056',
+ 'The formatted time value should be displayed properly.');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('button.o_field_float_toggle').text(), '0.056',
+ 'The value should be rendered correctly on the button.');
+
+ form.$('button.o_field_float_toggle').click(); // clicking will make the next value 1, since 0 was the closest of 0.056
+
+ assert.strictEqual(form.$('button.o_field_float_toggle').text(), '1.000',
+ 'The value should be rendered correctly on the button.');
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '1.000',
+ 'The new value should be saved and displayed properly.');
+
+ form.destroy();
+ });
+
+
QUnit.module('PhoneWidget');
QUnit.test('phone field in form view on extra small screens', function (assert) {
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index 6cbfd3563dd..14647955afa 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -1418,6 +1418,26 @@ order.
- Supported field types: *float*
+- float_factor (FieldFloatFactor)
+ This widget aims to display properly a float value that converted using a factor
+ given in its options. So, for example, the value saved in database is 0.5 and the
+ factor is 3, the widget value should be formatted as 1.5.
+
+ - Supported field types: *float*
+
+- float_toggle (FieldFloatToggle)
+ The goal of this widget is to replace the input field by a button containing a
+ range of possible values (given in the options). Each click allows the user to loop
+ in the range. The purpose here is to restrict the field value to a predefined selection.
+ Also, the widget support the factor conversion as the *float_factor* widget (Range values
+ should be the result of the conversion).
+
+ - Supported field types: *float*
+
+ .. code-block:: xml
+
+
+
- boolean (FieldBoolean)
This is the default field type for fields of type *boolean*.