diff --git a/src/common.js b/src/common.js index 86da5621e7..50eb0c9cca 100644 --- a/src/common.js +++ b/src/common.js @@ -181,10 +181,155 @@ return date; }; - Sao.common.text_to_float_time = function(text, conversion, digit) { - // TODO + Sao.common.timedelta = {}; + Sao.common.timedelta.DEFAULT_CONVERTER = { + 's': 1 + }; + Sao.common.timedelta.DEFAULT_CONVERTER.m = + Sao.common.timedelta.DEFAULT_CONVERTER.s * 60; + Sao.common.timedelta.DEFAULT_CONVERTER.h = + Sao.common.timedelta.DEFAULT_CONVERTER.m * 60; + Sao.common.timedelta.DEFAULT_CONVERTER.d = + Sao.common.timedelta.DEFAULT_CONVERTER.h * 24; + Sao.common.timedelta.DEFAULT_CONVERTER.w = + Sao.common.timedelta.DEFAULT_CONVERTER.d * 7; + Sao.common.timedelta.DEFAULT_CONVERTER.M = + Sao.common.timedelta.DEFAULT_CONVERTER.w * 4; + Sao.common.timedelta.DEFAULT_CONVERTER.Y = + Sao.common.timedelta.DEFAULT_CONVERTER.d * 365; + Sao.common.timedelta._get_separator = function() { + // TODO translate + return { + Y: 'Y', + M: 'M', + w: 'w', + d: 'd', + h: 'h', + m: 'm', + s: 's' + }; + }; + Sao.common.timedelta.format = function(value, converter) { + if (!value) { + return ''; + } + if (!converter) { + converter = Sao.common.timedelta.DEFAULT_CONVERTER; + } + var text = []; + value = value.asSeconds(); + var sign = ''; + if (value < 0) { + sign = '-'; + } + value = Math.abs(value); + converter = Object.keys(converter).map(function(key) { + return [key, converter[key]]; + }); + converter.sort(function(first, second) { + return second[1] - first[1]; + }); + var values = []; + var k, v; + for (var i = 0; i < converter.length; i++) { + k = converter[i][0]; + v = converter[i][1]; + var part = Math.floor(value / v); + value -= part * v; + values.push(part); + } + for (i = 0; i < converter.length - 3; i++) { + k = converter[i][0]; + v = values[i]; + if (v) { + text.push(v + Sao.common.timedelta._get_separator()[k]); + } + } + if (jQuery(values.slice(-3)).is(function(i, v) { return v; }) || + jQuery.isEmptyObject(text)) { + var time = values.slice(-3, -1); + time = ('00' + time[0]).slice(-2) + ':' + ('00' + time[1]).slice(-2); + if (values.slice(-1)[0] || value) { + time += ':' + ('00' + values.slice(-1)[0]).slice(-2); + } + text.push(time); + } + text = sign + text.reduce(function(p, c) { + if (p) { + return p + ' ' + c; + } else { + return c; + } + }); + if (value) { + if (!jQuery(values.slice(-3)).is(function(i, v) { return v; })) { + // Add space if no time + text += ' '; + } + text += ('' + value.toFixed(6)).slice(1); + } return text; }; + Sao.common.timedelta.parse = function(text, converter) { + if (!text) { + return null; + } + if (!converter) { + converter = Sao.common.timedelta.DEFAULT_CONVERTER; + } + var separators = Sao.common.timedelta._get_separator(); + var separator; + for (var k in separators) { + separator = separators[k]; + text = text.replace(separator, separator + ' '); + } + + var seconds = 0; + var sec; + var parts = text.split(' '); + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + if (part.contains(':')) { + var subparts = part.split(':'); + var subconverter = [ + converter.h, converter.m, converter.s]; + for (var j = 0; + j < Math.min(subparts.length, subconverter.length); + j ++) { + var t = subparts[j]; + var v = subconverter[j]; + sec = Math.abs(Number(t)) * v; + if (!isNaN(sec)) { + seconds += sec; + } + } + } else { + var found = false; + for (var key in separators) { + separator =separators[key]; + if (part.endsWith(separator)) { + part = part.slice(0, -separator.length); + sec = Math.abs(parseInt(part, 10)) * converter[key]; + if (!isNaN(sec)) { + seconds += sec; + } + found = true; + break; + } + } + if (!found) { + sec = Math.abs(Number(part)); + if (!isNaN(sec)) { + seconds += sec; + } + } + } + } + if (text.contains('-')) { + seconds *= -1; + } + return Sao.TimeDelta(null, seconds); + }; Sao.common.ModelAccess = Sao.class_(Object, { init: function() { diff --git a/src/model.js b/src/model.js index eb05f129a5..8a32520b4c 100644 --- a/src/model.js +++ b/src/model.js @@ -983,6 +983,8 @@ return Sao.field.Date; case 'time': return Sao.field.Time; + case 'timedelta': + return Sao.field.TimeDelta; case 'float': return Sao.field.Float; case 'numeric': @@ -1297,6 +1299,26 @@ } }); + Sao.field.TimeDelta = Sao.class_(Sao.field.Field, { + _default: null, + converter: function(record) { + // TODO allow local context converter + return record.model.session.context[this.description.converter]; + }, + set_client: function(record, value, force_change) { + if (typeof(value) == 'string') { + value = Sao.common.timedelta.parse(value, this.converter(record)); + } + Sao.field.TimeDelta._super.set_client.call( + this, record, value, force_change); + }, + get_client: function(record) { + var value = Sao.field.TimeDelta._super.get_client.call( + this, record); + return Sao.common.timedelta.format(value, this.converter(record)); + } + }); + Sao.field.Float = Sao.class_(Sao.field.Field, { _default: null, default_digits: [16, 2], diff --git a/src/rpc.js b/src/rpc.js index 8c8352920b..61ff57e1b8 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -123,6 +123,9 @@ value = Sao.Time(value.hour, value.minute, value.second, value.microsecond / 1000); break; + case 'timedelta': + value = Sao.TimeDelta(null, value.seconds); + break; case 'buffer': // javascript's atob does not understand linefeed // characters @@ -195,6 +198,11 @@ 'second': value.second(), 'microsecond': value.millisecond() * 1000 }; + } else if (value.isTimeDelta) { + value = { + '__class__': 'timedelta', + 'seconds': value.asSeconds() + }; } else if (value instanceof Sao.Decimal) { value = { '__class__': 'Decimal', diff --git a/src/sao.js b/src/sao.js index 00d8a4b037..b2470f66a6 100644 --- a/src/sao.js +++ b/src/sao.js @@ -120,6 +120,20 @@ var Sao = {}; return time; }; + Sao.TimeDelta = function(days, seconds, + milliseconds, minutes, hours, weeks) { + var timedelta = moment.duration({ + days: days, + seconds: seconds, + milliseconds: milliseconds, + minutes: minutes, + hours: hours, + weeks: weeks + }); + timedelta.isTimeDelta = true; + return timedelta; + }; + Sao.config = {}; Sao.config.limit = 1000; Sao.config.display_size = 20; diff --git a/src/view.js b/src/view.js index a05134bf4f..01e542d2d9 100644 --- a/src/view.js +++ b/src/view.js @@ -57,6 +57,8 @@ return Sao.View.Tree.DateColumn; case 'time': return Sao.View.Tree.TimeColumn; + case 'timedelta': + return Sao.View.Tree.TimeDeltaColumn; case 'one2many': return Sao.View.Tree.One2ManyColumn; case 'many2many': @@ -68,8 +70,6 @@ case 'float': case 'numeric': return Sao.View.Tree.FloatColumn; - case 'float_time': - return Sao.View.Tree.FloatTimeColumn; case 'integer': case 'biginteger': return Sao.View.Tree.IntegerColumn; @@ -1048,6 +1048,10 @@ } }); + Sao.View.Tree.TimeDeltaColumn = Sao.class_(Sao.View.Tree.CharColumn, { + class_: 'column-timedelta' + }); + Sao.View.Tree.One2ManyColumn = Sao.class_(Sao.View.Tree.CharColumn, { class_: 'column-one2many', update_text: function(cell, record) { @@ -1059,19 +1063,6 @@ class_: 'column-many2many' }); - Sao.View.Tree.FloatTimeColumn = Sao.class_(Sao.View.Tree.CharColumn, { - class_: 'column-float_time', - init: function(model, attributes) { - Sao.View.Tree.FloatTimeColumn._super.init.call(this, model, - attributes); - this.conv = null; // TODO - }, - update_text: function(cell, record) { - cell.text(Sao.common.text_to_float_time( - this.field.get_client(record), this.conv)); - } - }); - Sao.View.Tree.ImageColumn = Sao.class_(Sao.View.Tree.CharColumn, { class_: 'column-image', get_cell: function() { @@ -1756,6 +1747,8 @@ return Sao.View.Form.DateTime; case 'time': return Sao.View.Form.Time; + case 'timedelta': + return Sao.View.Form.TimeDelta; case 'integer': case 'biginteger': return Sao.View.Form.Integer; @@ -1764,8 +1757,6 @@ return Sao.View.Form.Float; case 'selection': return Sao.View.Form.Selection; - case 'float_time': - return Sao.View.Form.FloatTime; case 'boolean': return Sao.View.Form.Boolean; case 'text': @@ -2012,6 +2003,37 @@ } }); + Sao.View.Form.TimeDelta = Sao.class_(Sao.View.Form.Widget, { + class_: 'form-timedelta', + init: function(field_name, model, attributes) { + Sao.View.Form.TimeDelta._super.init.call(this, field_name, model, + attributes); + this.el = jQuery('
', { + 'class': this.class_ + }); + this.input = jQuery('', { + 'type': 'text', + 'class': 'form-control input-sm' + }).appendTo(this.el); + this.el.change(this.focus_out.bind(this)); + }, + display: function(record, field) { + Sao.View.Form.TimeDelta._super.display.call(this, record, field); + if (record) { + var value = record.field_get_client(this.field_name); + this.input.val(value || ''); + } else { + this.input.val(''); + } + }, + set_value: function(record, field) { + field.set_client(record, this.input.val()); + }, + set_readonly: function(readonly) { + this.input.prop('disabled', readonly); + } + }); + Sao.View.Form.Integer = Sao.class_(Sao.View.Form.Char, { class_: 'form-integer', init: function(field_name, model, attributes) { @@ -2130,24 +2152,6 @@ } }); - Sao.View.Form.FloatTime = Sao.class_(Sao.View.Form.Char, { - class_: 'form-float-time', - init: function(field_name, model, attributes) { - Sao.View.Form.FloatTime._super.init.call(this, field_name, model, - attributes); - this.conv = null; // TODO - }, - display: function(record, field) { - Sao.View.Form.FloatTime._super.display.call(this, record, field); - if (record) { - var value = record.field_get_client(this.field_name); - this.input.val(Sao.common.text_to_float_time(value, this.conv)); - } else { - this.input.val(''); - } - } - }); - Sao.View.Form.Boolean = Sao.class_(Sao.View.Form.Widget, { class_: 'form-boolean', init: function(field_name, model, attributes) { @@ -3368,8 +3372,6 @@ return Sao.View.EditableTree.Float; case 'selection': return Sao.View.EditableTree.Selection; - case 'float_time': - return Sao.View.EditableTree.FloatTime; case 'boolean': return Sao.View.EditableTree.Boolean; case 'many2one': @@ -3420,6 +3422,15 @@ } }); + Sao.View.EditableTree.TimeDelta = Sao.class_(Sao.View.Form.TimeDelta, { + class_: 'editabletree-timedelta', + init: function(field_name, model, attributes) { + Sao.View.EditableTree.TimeDelta._super.init.call(this, field_name, + model, attributes); + Sao.View.EditableTree.editable_mixin(this); + } + }); + Sao.View.EditableTree.Integer = Sao.class_(Sao.View.Form.Integer, { class_: 'editabletree-integer', init: function(field_name, model, attributes) { @@ -3447,15 +3458,6 @@ } }); - Sao.View.EditableTree.FloatTime = Sao.class_(Sao.View.Form.FloatTime, { - class_: 'editabletree-float_time', - init: function(field_name, model, attributes) { - Sao.View.EditableTree.FloatTime._super.init.call(this, field_name, - model, attributes); - Sao.View.EditableTree.editable_mixin(this); - } - }); - Sao.View.EditableTree.Boolean = Sao.class_(Sao.View.Form.Boolean, { class_: 'editabletree-boolean', init: function(field_name, model, attributes) { diff --git a/tests/sao.js b/tests/sao.js index 3860c06118..ddb6daf505 100644 --- a/tests/sao.js +++ b/tests/sao.js @@ -840,6 +840,45 @@ }); }); + var timedelta_tests = [ + [null, ''], + [Sao.TimeDelta(3, 0, 0, 30, 5), '3d 05:30'], + [Sao.TimeDelta(0, 0, 0, 0, 0, 48), '12M'], + [Sao.TimeDelta(0, 0, 0, 0, 0, 50), '12M 2w'], + [Sao.TimeDelta(365), '1Y'], + [Sao.TimeDelta(0, 10, 0, 5, 2), '02:05:10'], + [Sao.TimeDelta(0, 0, 42, 15), '00:15:00.042000'], + [Sao.TimeDelta(1, 0, 42), '1d .042000'], + [Sao.TimeDelta(0, -1), '-00:00:01'], + [Sao.TimeDelta(-1, 0, 0, -30, -5), '-1d 05:30'] + ]; + + QUnit.test('timedelta.format', function() { + timedelta_tests.forEach(function(test) { + var timedelta = test[0]; + var text = test[1]; + QUnit.equal(Sao.common.timedelta.format(timedelta), text, + 'timedelta.format(' + JSON.stringify(timedelta) + ')'); + }); + }); + + QUnit.test('timedelta.parse', function() { + function asSeconds(timedelta) { + if (timedelta) { + return timedelta.asSeconds(); + } else { + return timedelta; + } + } + timedelta_tests.forEach(function(test) { + var timedelta = test[0]; + var text = test[1]; + QUnit.equal(asSeconds(Sao.common.timedelta.parse(text)), + asSeconds(timedelta), + 'timedelta.format(' + JSON.stringify(timedelta) + ')'); + }); + }); + QUnit.test('DomainParser.group', function() { var parser = new Sao.common.DomainParser({ 'name': {