diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 9f37240e0cc..85cd0382ac2 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -282,6 +282,14 @@ var FieldDate = InputField.extend({ // Private //-------------------------------------------------------------------------- + /** + * @override + * @private + */ + _doDebouncedAction: function () { + this.datewidget.change_datetime(); + }, + /** * return the datepicker value * @@ -311,6 +319,16 @@ var FieldDate = InputField.extend({ this.$input = this.datewidget.$input; }, + /** + * FieldDateTime overrides _formatValue to use the session timezone + * + * @override + * @private + * @param {Moment} value + */ + _formatValue: function (value) { + return field_utils.format.date(value, {timezone: true}); + }, }); var FieldDateTime = FieldDate.extend({ diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js index d6677291d47..b00fca0048a 100644 --- a/addons/web/static/src/js/fields/field_utils.js +++ b/addons/web/static/src/js/fields/field_utils.js @@ -60,12 +60,20 @@ function formatChar(value) { * an empty string. Note that this is dependant on the localization settings * * @param {Moment|false} + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.timezone=true] use the user timezone when formating the + * date * @returns {string} */ -function formatDate(value) { +function formatDate(value, field, options) { if (!value) { return ""; } + if (!options || !('timezone' in options) || options.timezone) { + value = value.clone().add(session.tzOffset < 0 ? -1 : 0, 'days'); + } var l10n = core._t.database.parameters; var date_format = time.strftime_to_moment_format(l10n.date_format); return value.format(date_format); @@ -77,9 +85,14 @@ function formatDate(value) { * settings * * @params {Moment|false} + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.timezone=true] use the user timezone when formating the + * date * @returns {string} */ -function formatDateTime(value, options) { +function formatDateTime(value, field, options) { if (!value) { return ""; } @@ -264,23 +277,32 @@ function formatSelection(value, field) { //////////////////////////////////////////////////////////////////////////////// /** - * create an Date object + * Create an Date object * The method toJSON return the formated value to send value server side * - * @params {string} + * @param {string} + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.isUTC] the formatted date is utc + * @param {boolean} [options.timezone=false] format the date after apply the timezone + * offset * @returns {Moment|false} Moment date object */ function parseDate(value, field, options) { if (!value) { return false; } - var date_pattern = time.strftime_to_moment_format(core._t.database.parameters.date_format); - var date_pattern_wo_zero = date_pattern.replace('MM','M').replace('DD','D'); + var datePattern = time.strftime_to_moment_format(core._t.database.parameters.date_format); + var datePatternWoZero = datePattern.replace('MM','M').replace('DD','D'); var date; if (options && options.isUTC) { date = moment.utc(value); } else { - date = moment.utc(value, [date_pattern, date_pattern_wo_zero, moment.ISO_8601], true); + date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601], true); + if (options && options.timezone) { + date.add(session.tzOffset > 0 ? -1 : 0, 'days'); + } } if (date.isValid() && date.year() >= 1900) { if (date.year() === 0) { @@ -297,22 +319,28 @@ function parseDate(value, field, options) { } /** - * create an Date object + * Create an Date object * The method toJSON return the formated value to send value server side * - * @params {string} + * @param {string} + * @param {Object} [field] + * a description of the field (note: this parameter is ignored) + * @param {Object} [options] additional options + * @param {boolean} [options.isUTC] the formatted date is utc + * @param {boolean} [options.timezone=false] format the date after apply the timezone + * offset * @returns {Moment|false} Moment date object */ function parseDateTime(value, field, options) { if (!value) { return false; } - var date_pattern = time.strftime_to_moment_format(core._t.database.parameters.date_format), - time_pattern = time.strftime_to_moment_format(core._t.database.parameters.time_format); - var date_pattern_wo_zero = date_pattern.replace('MM','M').replace('DD','D'), - time_pattern_wo_zero = time_pattern.replace('HH','H').replace('mm','m').replace('ss','s'); - var pattern1 = date_pattern + ' ' + time_pattern; - var pattern2 = date_pattern_wo_zero + ' ' + time_pattern_wo_zero; + var datePattern = time.strftime_to_moment_format(core._t.database.parameters.date_format), + timePattern = time.strftime_to_moment_format(core._t.database.parameters.time_format); + var datePatternWoZero = datePattern.replace('MM','M').replace('DD','D'), + timePatternWoZero = timePattern.replace('HH','H').replace('mm','m').replace('ss','s'); + var pattern1 = datePattern + ' ' + timePattern; + var pattern2 = datePatternWoZero + ' ' + timePatternWoZero; var datetime; if (options && options.isUTC) { // phatomjs crash if we don't use this format diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 1641f13f634..1aaf05ee11e 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -60,7 +60,8 @@ var DateWidget = Widget.extend({ } }, get_value: function() { - return this.get('value') && this.get('value').clone(); + var value = this.get('value'); + return value && value.clone(); }, set_value_from_ui: function() { var value = this.$input.val() || false; @@ -84,10 +85,10 @@ var DateWidget = Widget.extend({ } }, parse_client: function(v) { - return field_utils.parse[this.type_of_date](v, {timezone: false}); + return field_utils.parse[this.type_of_date](v, null, {timezone: false}); }, format_client: function(v) { - return field_utils.format[this.type_of_date](v, {timezone: false}); + return field_utils.format[this.type_of_date](v, null, {timezone: false}); }, set_datetime_default: function() { //when opening datetimepicker the date and time by default should be the one from diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 7d7aabb5617..1dd84ce3dd4 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1041,7 +1041,10 @@ QUnit.module('basic_fields', { '', translateParameters: { // Avoid issues due to localization formats date_format: '%m/%d/%Y', - } + }, + session: { + tzOffset: 0 + }, }); var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first(); diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 7813320af1d..a2347ad801b 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -33,8 +33,10 @@ QUnit.module('Views', { user_id: {string: "user", type: "many2one", relation: 'user'}, parnter_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.parnter_id'}, name: {string: "name", type: "char"}, - start: {string: "start", type: "datetime"}, - stop: {string: "stop", type: "datetime"}, + start_date: {string: "start date", type: "date"}, + stop_date: {string: "stop date", type: "date"}, + start: {string: "start datetime", type: "datetime"}, + stop: {string: "stop datetime", type: "datetime"}, allday: {string: "allday", type: "boolean"}, partner_ids: {string: "attendees", type: "one2many", relation: 'partner'}, type: {string: "type", type: "integer"}, @@ -95,32 +97,19 @@ QUnit.module('Views', { QUnit.module('CalendarView'); var archs = { - "event,false,form": { - attrs: {}, - children: [ - { - attrs: {name: "name"}, - children: [], - tag: 'field' - }, - { - attrs: {name: "allday"}, - children: [], - tag: 'field' - }, - { - attrs: {name: "start"}, - children: [], - tag: 'field' - }, - { - attrs: {name: "stop"}, - children: [], - tag: 'field' - } - ], - tag: "form" - }, + "event,false,form": + '
'+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '', "event,1,form": { attrs: {}, children: [ @@ -337,10 +326,92 @@ QUnit.module('Views', { }); QUnit.test('create event with timezone in week mode', function (assert) { - assert.expect(8); + assert.expect(4); this.data.event.records = []; + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + ''+ + ''+ + '', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + session: { + tzOffset: 120 + }, + }); + + + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); // => select with click position + + var top = calendar.$('.fc-axis:contains(8am)').offset().top + 5; + var left = calendar.$('.fc-day:eq(2)').offset().left + 5; + + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + testUtils.triggerPositionalMouseEvent(left, top + 60, "mousemove"); + + assert.strictEqual(calendar.$('.fc-content .fc-time').text(), "08:00 - 10:00", + "should display the time in the calendar sticker"); + + testUtils.triggerPositionalMouseEvent(left, top + 60, "mouseup"); + $('.modal input:first').val('new event').trigger('input'); + $('.modal button.btn:contains(Create)').trigger('click'); + var $newevent = calendar.$('.fc-event:contains(new event)'); + + assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-10:00newevent", + "should display the new event with time and title"); + + assert.deepEqual($newevent.data('fcSeg').event.record, + { + display_name: "new event", + start: "2016-12-13 06:00:00", + stop: "2016-12-13 08:00:00", + allday: false, + name: "new event", + id: 1 + }, + "the new record should have the utc datetime (quickCreate)"); + + // delete record + + $newevent.trigger('click'); + $('.modal button.btn-default:contains(Delete)').trigger('click'); + $('.modal button.btn-primary:contains(Ok)').trigger('click'); + assert.strictEqual(calendar.$('.fc-content').length, 0, "should delete the record"); + + calendar.destroy(); + $view.remove(); + }); + + QUnit.test('create event with timezone in week mode with formViewDialog', function (assert) { + assert.expect(6); + + this.data.event.records = []; + this.data.event.onchanges = { + allday: function (obj) { + if (obj.allday) { + obj.start_date = obj.start && obj.start.split(' ')[0] || obj.start_date; + obj.stop_date = obj.stop && obj.stop.split(' ')[0] || obj.stop_date || obj.start_date; + } else { + obj.start = obj.start_date && (obj.start_date + ' 00:00:00') || obj.start; + obj.stop = obj.stop_date && (obj.stop_date + ' 00:00:00') || obj.stop || obj.start; + } + } + }; + var calendar = createView({ View: CalendarView, model: 'event', @@ -367,7 +438,7 @@ QUnit.module('Views', { assert.deepEqual(args.args[1], { "allday": false, "start": "2016-12-12 06:00:00", - "stop": "2016-12-12 10:00:00" + "stop": "2016-12-12 08:00:00" }, "should move the event"); } @@ -375,75 +446,66 @@ QUnit.module('Views', { }, }); - var $view = $('#qunit-fixture').contents(); $view.prependTo('body'); // => select with click position var top = calendar.$('.fc-axis:contains(8am)').offset().top + 5; var left = calendar.$('.fc-day:eq(2)').offset().left + 5; - testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); - testUtils.triggerPositionalMouseEvent(left, top + 140, "mousemove"); - - assert.strictEqual(calendar.$('.fc-content .fc-time').text(), "08:00 - 12:00", - "should display the time in the calendar sticker"); - - testUtils.triggerPositionalMouseEvent(left, top + 140, "mouseup"); - $('.modal input:first').val('new event').trigger('input'); - $('.modal button.btn:contains(Create)').trigger('click'); - var $newevent = calendar.$('.fc-event:contains(new event)'); - - assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-12:00newevent", - "should display the new event with time and title"); - - assert.deepEqual($newevent.data('fcSeg').event.record, - { - display_name: "new event", - start: "2016-12-13 06:00:00", - stop: "2016-12-13 10:00:00", - allday: false, - name: "new event", - id: 1 - }, - "the new record should have the utc datetime (quickCreate)"); - - // delete record - - $newevent.trigger('click'); - $('.modal button.btn-default:contains(Delete)').trigger('click'); - $('.modal button.btn-primary:contains(Ok)').trigger('click'); - assert.strictEqual(calendar.$('.fc-content').length, 0, "should delete the record"); - - - // create again - - testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); - testUtils.triggerPositionalMouseEvent(left, top + 140, "mousemove"); - testUtils.triggerPositionalMouseEvent(left, top + 140, "mouseup"); + testUtils.triggerPositionalMouseEvent(left, top + 60, "mousemove"); + testUtils.triggerPositionalMouseEvent(left, top + 60, "mouseup"); $('.modal input:first').val('new event').trigger('input'); $('.modal button.btn:contains(Edit)').trigger('click'); assert.strictEqual($('.o_form_field[name="start"] input').val(), "12/13/2016 08:00:00", "should display the datetime"); - $('.modal-lg button.btn:contains(Save)').trigger('click'); - $newevent = calendar.$('.fc-event:contains(new event)'); + $('.modal-lg .o_field_boolean[name="allday"] input').trigger('click'); - assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-12:00newevent", + assert.strictEqual($('.o_form_field[name="start_date"] input').val(), "12/13/2016", + "should display the date"); + + $('.modal-lg .o_field_boolean[name="allday"] input').trigger('click'); + + assert.strictEqual($('.o_form_field[name="start"] input').val(), "12/13/2016 02:00:00", + "should display the datetime from the date with the timezone"); + + // use datepicker to enter a date: 12/13/2016 08:00:00 + $('.o_form_field[name="start"] input').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(08)').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click'); + + // use datepicker to enter a date: 12/13/2016 10:00:00 + $('.o_form_field[name="stop"] input').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(10)').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click'); + + $('.modal-lg button.btn:contains(Save)').trigger('click'); + var $newevent = calendar.$('.fc-event:contains(new event)'); + + assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "08:00-10:00newevent", "should display the new event with time and title"); assert.deepEqual($newevent.data('fcSeg').event.record, { display_name: "new event", start: "2016-12-13 06:00:00", - stop: "2016-12-13 10:00:00", + stop: "2016-12-13 08:00:00", allday: false, name: "new event", id: 1 }, "the new record should have the utc datetime (formViewDialog)"); + var pos = calendar.$('.fc-content').offset(); + left = pos.left + 5; + top = pos.top + 5; + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); left = calendar.$('.fc-day:eq(1)').offset().left + 5; testUtils.triggerPositionalMouseEvent(left, top, "mousemove");