';
+ html += '';
+ html += '';
+
+ //adjust maxDate to reflect the maxSpan setting in order to
+ //grey out end dates beyond the maxSpan
+ if (this.endDate == null && this.maxSpan) {
+ var maxLimit = this.startDate.clone().add(this.maxSpan).endOf('day');
+ if (!maxDate || maxLimit.isBefore(maxDate)) {
+ maxDate = maxLimit;
+ }
+ }
+
+ for (var row = 0; row < 6; row++) {
+ html += '
';
+
+ // add week number
+ if (this.showWeekNumbers)
+ html += '
' + calendar[row][0].week() + '
';
+ else if (this.showISOWeekNumbers)
+ html += '
' + calendar[row][0].isoWeek() + '
';
+
+ for (var col = 0; col < 7; col++) {
+
+ var classes = [];
+
+ //highlight today's date
+ if (calendar[row][col].isSame(new Date(), "day"))
+ classes.push('today');
+
+ //highlight weekends
+ if (calendar[row][col].isoWeekday() > 5)
+ classes.push('weekend');
+
+ //grey out the dates in other months displayed at beginning and end of this calendar
+ if (calendar[row][col].month() != calendar[1][1].month())
+ classes.push('off', 'ends');
+
+ //don't allow selection of dates before the minimum date
+ if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day'))
+ classes.push('off', 'disabled');
+
+ //don't allow selection of dates after the maximum date
+ if (maxDate && calendar[row][col].isAfter(maxDate, 'day'))
+ classes.push('off', 'disabled');
+
+ //don't allow selection of date if a custom function decides it's invalid
+ if (this.isInvalidDate(calendar[row][col]))
+ classes.push('off', 'disabled');
+
+ //highlight the currently selected start date
+ if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD'))
+ classes.push('active', 'start-date');
+
+ //highlight the currently selected end date
+ if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD'))
+ classes.push('active', 'end-date');
+
+ //highlight dates in-between the selected dates
+ if (this.endDate != null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate)
+ classes.push('in-range');
+
+ //apply custom classes for this date
+ var isCustom = this.isCustomDate(calendar[row][col]);
+ if (isCustom !== false) {
+ if (typeof isCustom === 'string')
+ classes.push(isCustom);
+ else
+ Array.prototype.push.apply(classes, isCustom);
+ }
+
+ var cname = '', disabled = false;
+ for (var i = 0; i < classes.length; i++) {
+ cname += classes[i] + ' ';
+ if (classes[i] == 'disabled')
+ disabled = true;
+ }
+ if (!disabled)
+ cname += 'available';
+
+ html += '
' + calendar[row][col].date() + '
';
+
+ }
+ html += '
';
+ }
+
+ html += '';
+ html += '
';
+
+ this.container.find('.drp-calendar.' + side + ' .calendar-table').html(html);
+
+ },
+
+ renderTimePicker: function(side) {
+
+ // Don't bother updating the time picker if it's currently disabled
+ // because an end date hasn't been clicked yet
+ if (side == 'right' && !this.endDate) return;
+
+ var html, selected, minDate, maxDate = this.maxDate;
+
+ if (this.maxSpan && (!this.maxDate || this.startDate.clone().add(this.maxSpan).isBefore(this.maxDate)))
+ maxDate = this.startDate.clone().add(this.maxSpan);
+
+ if (side == 'left') {
+ selected = this.startDate.clone();
+ minDate = this.minDate;
+ } else if (side == 'right') {
+ selected = this.endDate.clone();
+ minDate = this.startDate;
+
+ //Preserve the time already selected
+ var timeSelector = this.container.find('.drp-calendar.right .calendar-time');
+ if (timeSelector.html() != '') {
+
+ selected.hour(!isNaN(selected.hour()) ? selected.hour() : timeSelector.find('.hourselect option:selected').val());
+ selected.minute(!isNaN(selected.minute()) ? selected.minute() : timeSelector.find('.minuteselect option:selected').val());
+ selected.second(!isNaN(selected.second()) ? selected.second() : timeSelector.find('.secondselect option:selected').val());
+
+ if (!this.timePicker24Hour) {
+ var ampm = timeSelector.find('.ampmselect option:selected').val();
+ if (ampm === 'PM' && selected.hour() < 12)
+ selected.hour(selected.hour() + 12);
+ if (ampm === 'AM' && selected.hour() === 12)
+ selected.hour(0);
+ }
+
+ }
+
+ if (selected.isBefore(this.startDate))
+ selected = this.startDate.clone();
+
+ if (maxDate && selected.isAfter(maxDate))
+ selected = maxDate.clone();
+
+ }
+
+ //
+ // hours
+ //
+
+ html = ' ';
+
+ //
+ // minutes
+ //
+
+ html += ': ';
+
+ //
+ // seconds
+ //
+
+ if (this.timePickerSeconds) {
+ html += ': ';
+ }
+
+ //
+ // AM/PM
+ //
+
+ if (!this.timePicker24Hour) {
+ html += '';
+ }
+
+ this.container.find('.drp-calendar.' + side + ' .calendar-time').html(html);
+
+ },
+
+ updateFormInputs: function() {
+
+ if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) {
+ this.container.find('button.applyBtn').removeAttr('disabled');
+ } else {
+ this.container.find('button.applyBtn').attr('disabled', 'disabled');
+ }
+
+ },
+
+ move: function() {
+ var parentOffset = { top: 0, left: 0 },
+ containerTop;
+ var parentRightEdge = $(window).width();
+ if (!this.parentEl.is('body')) {
+ parentOffset = {
+ top: this.parentEl.offset().top - this.parentEl.scrollTop(),
+ left: this.parentEl.offset().left - this.parentEl.scrollLeft()
+ };
+ parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left;
+ }
+
+ if (this.drops == 'up')
+ containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top;
+ else
+ containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top;
+
+ // Force the container to it's actual width
+ this.container.css({
+ top: 0,
+ left: 0,
+ right: 'auto'
+ });
+ var containerWidth = this.container.outerWidth();
+
+ this.container[this.drops == 'up' ? 'addClass' : 'removeClass']('drop-up');
+
+ if (this.opens == 'left') {
+ var containerRight = parentRightEdge - this.element.offset().left - this.element.outerWidth();
+ if (containerWidth + containerRight > $(window).width()) {
+ this.container.css({
+ top: containerTop,
+ right: 'auto',
+ left: 9
+ });
+ } else {
+ this.container.css({
+ top: containerTop,
+ right: containerRight,
+ left: 'auto'
+ });
+ }
+ } else if (this.opens == 'center') {
+ var containerLeft = this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2
+ - containerWidth / 2;
+ if (containerLeft < 0) {
+ this.container.css({
+ top: containerTop,
+ right: 'auto',
+ left: 9
+ });
+ } else if (containerLeft + containerWidth > $(window).width()) {
+ this.container.css({
+ top: containerTop,
+ left: 'auto',
+ right: 0
+ });
+ } else {
+ this.container.css({
+ top: containerTop,
+ left: containerLeft,
+ right: 'auto'
+ });
+ }
+ } else {
+ var containerLeft = this.element.offset().left - parentOffset.left;
+ if (containerLeft + containerWidth > $(window).width()) {
+ this.container.css({
+ top: containerTop,
+ left: 'auto',
+ right: 0
+ });
+ } else {
+ this.container.css({
+ top: containerTop,
+ left: containerLeft,
+ right: 'auto'
+ });
+ }
+ }
+ },
+
+ show: function(e) {
+ if (this.isShowing) return;
+
+ // Create a click proxy that is private to this instance of datepicker, for unbinding
+ this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this);
+
+ // Bind global datepicker mousedown for hiding and
+ $(document)
+ .on('mousedown.daterangepicker', this._outsideClickProxy)
+ // also support mobile devices
+ .on('touchend.daterangepicker', this._outsideClickProxy)
+ // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them
+ .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy)
+ // and also close when focus changes to outside the picker (eg. tabbing between controls)
+ .on('focusin.daterangepicker', this._outsideClickProxy);
+
+ // Reposition the picker if the window is resized while it's open
+ $(window).on('resize.daterangepicker', $.proxy(function(e) { this.move(e); }, this));
+
+ this.oldStartDate = this.startDate.clone();
+ this.oldEndDate = this.endDate.clone();
+ this.previousRightTime = this.endDate.clone();
+
+ this.updateView();
+ this.container.show();
+ this.move();
+ this.element.trigger('show.daterangepicker', this);
+ this.isShowing = true;
+ },
+
+ hide: function(e) {
+ if (!this.isShowing) return;
+
+ //incomplete date selection, revert to last values
+ if (!this.endDate) {
+ this.startDate = this.oldStartDate.clone();
+ this.endDate = this.oldEndDate.clone();
+ }
+
+ //if a new date range was selected, invoke the user callback function
+ if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate))
+ this.callback(this.startDate.clone(), this.endDate.clone(), this.chosenLabel);
+
+ //if picker is attached to a text input, update it
+ this.updateElement();
+
+ $(document).off('.daterangepicker');
+ $(window).off('.daterangepicker');
+ this.container.hide();
+ this.element.trigger('hide.daterangepicker', this);
+ this.isShowing = false;
+ },
+
+ toggle: function(e) {
+ if (this.isShowing) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ },
+
+ outsideClick: function(e) {
+ var target = $(e.target);
+ // if the page is clicked anywhere except within the daterangerpicker/button
+ // itself then call this.hide()
+ if (
+ // ie modal dialog fix
+ e.type == "focusin" ||
+ target.closest(this.element).length ||
+ target.closest(this.container).length ||
+ target.closest('.calendar-table').length
+ ) return;
+ this.hide();
+ this.element.trigger('outsideClick.daterangepicker', this);
+ },
+
+ showCalendars: function() {
+ this.container.addClass('show-calendar');
+ this.move();
+ this.element.trigger('showCalendar.daterangepicker', this);
+ },
+
+ hideCalendars: function() {
+ this.container.removeClass('show-calendar');
+ this.element.trigger('hideCalendar.daterangepicker', this);
+ },
+
+ clickRange: function(e) {
+ var label = e.target.getAttribute('data-range-key');
+ this.chosenLabel = label;
+ if (label == this.locale.customRangeLabel) {
+ this.showCalendars();
+ } else {
+ var dates = this.ranges[label];
+ this.startDate = dates[0];
+ this.endDate = dates[1];
+
+ if (!this.timePicker) {
+ this.startDate.startOf('day');
+ this.endDate.endOf('day');
+ }
+
+ if (!this.alwaysShowCalendars)
+ this.hideCalendars();
+ this.clickApply();
+ }
+ },
+
+ clickPrev: function(e) {
+ var cal = $(e.target).parents('.drp-calendar');
+ if (cal.hasClass('left')) {
+ this.leftCalendar.month.subtract(1, 'month');
+ if (this.linkedCalendars)
+ this.rightCalendar.month.subtract(1, 'month');
+ } else {
+ this.rightCalendar.month.subtract(1, 'month');
+ }
+ this.updateCalendars();
+ },
+
+ clickNext: function(e) {
+ var cal = $(e.target).parents('.drp-calendar');
+ if (cal.hasClass('left')) {
+ this.leftCalendar.month.add(1, 'month');
+ } else {
+ this.rightCalendar.month.add(1, 'month');
+ if (this.linkedCalendars)
+ this.leftCalendar.month.add(1, 'month');
+ }
+ this.updateCalendars();
+ },
+
+ hoverDate: function(e) {
+
+ //ignore dates that can't be selected
+ if (!$(e.target).hasClass('available')) return;
+
+ var title = $(e.target).attr('data-title');
+ var row = title.substr(1, 1);
+ var col = title.substr(3, 1);
+ var cal = $(e.target).parents('.drp-calendar');
+ var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col];
+
+ //highlight the dates between the start date and the date being hovered as a potential end date
+ var leftCalendar = this.leftCalendar;
+ var rightCalendar = this.rightCalendar;
+ var startDate = this.startDate;
+ if (!this.endDate) {
+ this.container.find('.drp-calendar tbody td').each(function(index, el) {
+
+ //skip week numbers, only look at dates
+ if ($(el).hasClass('week')) return;
+
+ var title = $(el).attr('data-title');
+ var row = title.substr(1, 1);
+ var col = title.substr(3, 1);
+ var cal = $(el).parents('.drp-calendar');
+ var dt = cal.hasClass('left') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col];
+
+ if ((dt.isAfter(startDate) && dt.isBefore(date)) || dt.isSame(date, 'day')) {
+ $(el).addClass('in-range');
+ } else {
+ $(el).removeClass('in-range');
+ }
+
+ });
+ }
+
+ },
+
+ clickDate: function(e) {
+
+ if (!$(e.target).hasClass('available')) return;
+
+ var title = $(e.target).attr('data-title');
+ var row = title.substr(1, 1);
+ var col = title.substr(3, 1);
+ var cal = $(e.target).parents('.drp-calendar');
+ var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col];
+
+ //
+ // this function needs to do a few things:
+ // * alternate between selecting a start and end date for the range,
+ // * if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date
+ // * if autoapply is enabled, and an end date was chosen, apply the selection
+ // * if single date picker mode, and time picker isn't enabled, apply the selection immediately
+ // * if one of the inputs above the calendars was focused, cancel that manual input
+ //
+
+ if (this.endDate || date.isBefore(this.startDate, 'day')) { //picking start
+ if (this.timePicker) {
+ var hour = parseInt(this.container.find('.left .hourselect').val(), 10);
+ if (!this.timePicker24Hour) {
+ var ampm = this.container.find('.left .ampmselect').val();
+ if (ampm === 'PM' && hour < 12)
+ hour += 12;
+ if (ampm === 'AM' && hour === 12)
+ hour = 0;
+ }
+ var minute = parseInt(this.container.find('.left .minuteselect').val(), 10);
+ if (isNaN(minute)) {
+ minute = parseInt(this.container.find('.left .minuteselect option:last').val(), 10);
+ }
+ var second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0;
+ date = date.clone().hour(hour).minute(minute).second(second);
+ }
+ this.endDate = null;
+ this.setStartDate(date.clone());
+ } else if (!this.endDate && date.isBefore(this.startDate)) {
+ //special case: clicking the same date for start/end,
+ //but the time of the end date is before the start date
+ this.setEndDate(this.startDate.clone());
+ } else { // picking end
+ if (this.timePicker) {
+ var hour = parseInt(this.container.find('.right .hourselect').val(), 10);
+ if (!this.timePicker24Hour) {
+ var ampm = this.container.find('.right .ampmselect').val();
+ if (ampm === 'PM' && hour < 12)
+ hour += 12;
+ if (ampm === 'AM' && hour === 12)
+ hour = 0;
+ }
+ var minute = parseInt(this.container.find('.right .minuteselect').val(), 10);
+ if (isNaN(minute)) {
+ minute = parseInt(this.container.find('.right .minuteselect option:last').val(), 10);
+ }
+ var second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0;
+ date = date.clone().hour(hour).minute(minute).second(second);
+ }
+ this.setEndDate(date.clone());
+ if (this.autoApply) {
+ this.calculateChosenLabel();
+ this.clickApply();
+ }
+ }
+
+ if (this.singleDatePicker) {
+ this.setEndDate(this.startDate);
+ if (!this.timePicker)
+ this.clickApply();
+ }
+
+ this.updateView();
+
+ //This is to cancel the blur event handler if the mouse was in one of the inputs
+ e.stopPropagation();
+
+ },
+
+ calculateChosenLabel: function () {
+ var customRange = true;
+ var i = 0;
+ for (var range in this.ranges) {
+ if (this.timePicker) {
+ var format = this.timePickerSeconds ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD HH:mm";
+ //ignore times when comparing dates if time picker seconds is not enabled
+ if (this.startDate.format(format) == this.ranges[range][0].format(format) && this.endDate.format(format) == this.ranges[range][1].format(format)) {
+ customRange = false;
+ this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').attr('data-range-key');
+ break;
+ }
+ } else {
+ //ignore times when comparing dates if time picker is not enabled
+ if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) {
+ customRange = false;
+ this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').attr('data-range-key');
+ break;
+ }
+ }
+ i++;
+ }
+ if (customRange) {
+ if (this.showCustomRangeLabel) {
+ this.chosenLabel = this.container.find('.ranges li:last').addClass('active').attr('data-range-key');
+ } else {
+ this.chosenLabel = null;
+ }
+ this.showCalendars();
+ }
+ },
+
+ clickApply: function(e) {
+ this.hide();
+ this.element.trigger('apply.daterangepicker', this);
+ },
+
+ clickCancel: function(e) {
+ this.startDate = this.oldStartDate;
+ this.endDate = this.oldEndDate;
+ this.hide();
+ this.element.trigger('cancel.daterangepicker', this);
+ },
+
+ monthOrYearChanged: function(e) {
+ var isLeft = $(e.target).closest('.drp-calendar').hasClass('left'),
+ leftOrRight = isLeft ? 'left' : 'right',
+ cal = this.container.find('.drp-calendar.'+leftOrRight);
+
+ // Month must be Number for new moment versions
+ var month = parseInt(cal.find('.monthselect').val(), 10);
+ var year = cal.find('.yearselect').val();
+
+ if (!isLeft) {
+ if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) {
+ month = this.startDate.month();
+ year = this.startDate.year();
+ }
+ }
+
+ if (this.minDate) {
+ if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) {
+ month = this.minDate.month();
+ year = this.minDate.year();
+ }
+ }
+
+ if (this.maxDate) {
+ if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) {
+ month = this.maxDate.month();
+ year = this.maxDate.year();
+ }
+ }
+
+ if (isLeft) {
+ this.leftCalendar.month.month(month).year(year);
+ if (this.linkedCalendars)
+ this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month');
+ } else {
+ this.rightCalendar.month.month(month).year(year);
+ if (this.linkedCalendars)
+ this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month');
+ }
+ this.updateCalendars();
+ },
+
+ timeChanged: function(e) {
+
+ var cal = $(e.target).closest('.drp-calendar'),
+ isLeft = cal.hasClass('left');
+
+ var hour = parseInt(cal.find('.hourselect').val(), 10);
+ var minute = parseInt(cal.find('.minuteselect').val(), 10);
+ if (isNaN(minute)) {
+ minute = parseInt(cal.find('.minuteselect option:last').val(), 10);
+ }
+ var second = this.timePickerSeconds ? parseInt(cal.find('.secondselect').val(), 10) : 0;
+
+ if (!this.timePicker24Hour) {
+ var ampm = cal.find('.ampmselect').val();
+ if (ampm === 'PM' && hour < 12)
+ hour += 12;
+ if (ampm === 'AM' && hour === 12)
+ hour = 0;
+ }
+
+ if (isLeft) {
+ var start = this.startDate.clone();
+ start.hour(hour);
+ start.minute(minute);
+ start.second(second);
+ this.setStartDate(start);
+ if (this.singleDatePicker) {
+ this.endDate = this.startDate.clone();
+ } else if (this.endDate && this.endDate.format('YYYY-MM-DD') == start.format('YYYY-MM-DD') && this.endDate.isBefore(start)) {
+ this.setEndDate(start.clone());
+ }
+ } else if (this.endDate) {
+ var end = this.endDate.clone();
+ end.hour(hour);
+ end.minute(minute);
+ end.second(second);
+ this.setEndDate(end);
+ }
+
+ //update the calendars so all clickable dates reflect the new time component
+ this.updateCalendars();
+
+ //update the form inputs above the calendars with the new time
+ this.updateFormInputs();
+
+ //re-render the time pickers because changing one selection can affect what's enabled in another
+ this.renderTimePicker('left');
+ this.renderTimePicker('right');
+
+ },
+
+ elementChanged: function() {
+ if (!this.element.is('input')) return;
+ if (!this.element.val().length) return;
+
+ var dateString = this.element.val().split(this.locale.separator),
+ start = null,
+ end = null;
+
+ if (dateString.length === 2) {
+ start = moment(dateString[0], this.locale.format);
+ end = moment(dateString[1], this.locale.format);
+ }
+
+ if (this.singleDatePicker || start === null || end === null) {
+ start = moment(this.element.val(), this.locale.format);
+ end = start;
+ }
+
+ if (!start.isValid() || !end.isValid()) return;
+
+ this.setStartDate(start);
+ this.setEndDate(end);
+ this.updateView();
+ },
+
+ keydown: function(e) {
+ //hide on tab or enter
+ if ((e.keyCode === 9) || (e.keyCode === 13)) {
+ this.hide();
+ }
+
+ //hide on esc and prevent propagation
+ if (e.keyCode === 27) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.hide();
+ }
+ },
+
+ updateElement: function() {
+ if (this.element.is('input') && this.autoUpdateInput) {
+ var newValue = this.startDate.format(this.locale.format);
+ if (!this.singleDatePicker) {
+ newValue += this.locale.separator + this.endDate.format(this.locale.format);
+ }
+ if (newValue !== this.element.val()) {
+ this.element.val(newValue).trigger('change');
+ }
+ }
+ },
+
+ remove: function() {
+ this.container.remove();
+ this.element.off('.daterangepicker');
+ this.element.removeData();
+ }
+
+ };
+
+ $.fn.daterangepicker = function(options, callback) {
+ var implementOptions = $.extend(true, {}, $.fn.daterangepicker.defaultOptions, options);
+ this.each(function() {
+ var el = $(this);
+ if (el.data('daterangepicker'))
+ el.data('daterangepicker').remove();
+ el.data('daterangepicker', new DateRangePicker(el, implementOptions, callback));
+ });
+ return this;
+ };
+
+ return DateRangePicker;
+
+}));
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 78473258128..83b6d885898 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -22,6 +22,7 @@ var session = require('web.session');
var utils = require('web.utils');
var view_dialogs = require('web.view_dialogs');
var field_utils = require('web.field_utils');
+var time = require('web.time');
require("web.zoomodoo");
@@ -554,6 +555,121 @@ var LinkButton = AbstractField.extend({
},
});
+var FieldDateRange = InputField.extend({
+ className: 'o_field_date_range',
+ tagName: 'span',
+ jsLibs: [
+ '/web/static/lib/daterangepicker/daterangepicker.js',
+ '/web/static/src/js/libs/daterangepicker.js',
+ ],
+ cssLibs: [
+ '/web/static/lib/daterangepicker/daterangepicker.css',
+ ],
+ supportedFieldTypes: ['date', 'datetime'],
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.isDateField = this.formatType === 'date';
+ this.dateRangePickerOptions = _.defaults(
+ {},
+ this.nodeOptions.picker_options || {},
+ {
+ timePicker: !this.isDateField,
+ timePicker24Hour: _t.database.parameters.time_format.search('%H') !== -1,
+ autoUpdateInput: false,
+ timePickerIncrement: 10,
+ locale: {
+ format: this.isDateField ? time.getLangDateFormat() : time.getLangDatetimeFormat(),
+ },
+ }
+ );
+ this.relatedEndDate = this.nodeOptions.related_end_date;
+ this.relatedStartDate = this.nodeOptions.related_start_date;
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ if (this.$pickerContainer) {
+ this.$pickerContainer.remove();
+ }
+ this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ * @param {Object} picker
+ */
+ _applyChanges: function (ev, picker) {
+ var changes = {};
+ var displayStartDate = field_utils.format[this.formatType](picker.startDate, {}, {timezone: false});
+ var displayEndDate = field_utils.format[this.formatType](picker.endDate, {}, {timezone: false});
+ var changedStartDate = picker.startDate;
+ var changedEndDate = picker.endDate;
+ if (this.isDateField) {
+ // In date mode, the library will give moment object of start and end date having
+ // time at 00:00:00. So, Odoo will consider it as UTC. To fix this added browser
+ // timezone offset in dates to get a correct selected date.
+ changedStartDate = picker.startDate.add(session.getTZOffset(picker.startDate), 'minutes');
+ changedEndDate = picker.endDate.startOf('day').add(session.getTZOffset(picker.endDate), 'minutes');
+ }
+ if (this.relatedEndDate) {
+ this.$el.val(displayStartDate);
+ changes[this.name] = this._parseValue(changedStartDate);
+ changes[this.relatedEndDate] = this._parseValue(changedEndDate);
+ }
+ if (this.relatedStartDate) {
+ this.$el.val(displayEndDate);
+ changes[this.name] = this._parseValue(changedEndDate);
+ changes[this.relatedStartDate] = this._parseValue(changedStartDate);
+ }
+ this.trigger_up('field_changed', {
+ dataPointID: this.dataPointID,
+ viewType: this.viewType,
+ changes: changes,
+ });
+ },
+ /**
+ * @override
+ */
+ _renderEdit: function () {
+ this._super.apply(this, arguments);
+ var self = this;
+ var startDate;
+ var endDate;
+ if (this.relatedEndDate) {
+ startDate = this._formatValue(this.value);
+ endDate = this._formatValue(this.recordData[this.relatedEndDate]);
+ }
+ if (this.relatedStartDate) {
+ startDate = this._formatValue(this.recordData[this.relatedStartDate]);
+ endDate = this._formatValue(this.value);
+ }
+ this.dateRangePickerOptions.startDate = startDate || moment();
+ this.dateRangePickerOptions.endDate = endDate || moment();
+
+ this.$el.daterangepicker(this.dateRangePickerOptions);
+ this.$el.on('apply.daterangepicker', this._applyChanges.bind(this));
+ this.$el.off('keyup.daterangepicker');
+ this.$pickerContainer = this.$el.data('daterangepicker').container;
+
+ // Prevent from leaving the edition of a row in editable list view
+ this.$pickerContainer.on('click', function (ev) {
+ ev.stopPropagation();
+ if ($(ev.target).hasClass('applyBtn')) {
+ self.$el.data('daterangepicker').hide();
+ }
+ });
+ },
+});
+
var FieldDate = InputField.extend({
description: _lt("Date"),
className: "o_field_date",
@@ -3101,6 +3217,7 @@ return {
LinkButton: LinkButton,
FieldDate: FieldDate,
FieldDateTime: FieldDateTime,
+ FieldDateRange: FieldDateRange,
FieldDomain: FieldDomain,
FieldFloat: FieldFloat,
FieldFloatTime: FieldFloatTime,
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 4b1ce27aebd..b5eed8a42b5 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -24,6 +24,7 @@ registry
.add('boolean', basic_fields.FieldBoolean)
.add('date', basic_fields.FieldDate)
.add('datetime', basic_fields.FieldDateTime)
+ .add('daterange', basic_fields.FieldDateRange)
.add('domain', basic_fields.FieldDomain)
.add('text', basic_fields.FieldText)
.add('list.text', basic_fields.ListFieldText)
diff --git a/addons/web/static/src/js/libs/daterangepicker.js b/addons/web/static/src/js/libs/daterangepicker.js
new file mode 100644
index 00000000000..4d4acfeb2b9
--- /dev/null
+++ b/addons/web/static/src/js/libs/daterangepicker.js
@@ -0,0 +1,14 @@
+odoo.define('web.daterangepicker.extensions', function () {
+'use strict';
+
+/**
+ * Don't allow user to select off days(Dates which are out of current calendar).
+ */
+var clickDateFunction = daterangepicker.prototype.clickDate;
+daterangepicker.prototype.clickDate = function (ev) {
+ if (!$(ev.target).hasClass('off')) {
+ clickDateFunction.apply(this, arguments);
+ }
+};
+
+});
diff --git a/addons/web/static/src/scss/daterangepicker.scss b/addons/web/static/src/scss/daterangepicker.scss
new file mode 100644
index 00000000000..aece2a9903e
--- /dev/null
+++ b/addons/web/static/src/scss/daterangepicker.scss
@@ -0,0 +1,69 @@
+// Restore date range picker style
+.daterangepicker {
+ .drp-calendar {
+ .calendar-table {
+ thead {
+ tr:first-child {
+ color: #FFFFFF;
+ background-color: $o-brand-odoo;
+ th {
+ &.prev, &.next {
+ span {
+ color: #FFFFFF;
+ border-color: #FFFFFF;
+ }
+ &:hover {
+ background-color: darken($o-brand-odoo, 10%);
+ }
+ }
+ }
+ }
+ tr:last-child {
+ color: $o-datepicker-week-color;
+ background-color: $o-datepicker-week-bg-color;
+ }
+ th {
+ border-radius: 0;
+ }
+ }
+ tbody {
+ tr {
+ border-bottom: 1px solid $o-datepicker-week-bg-color;
+ td {
+ border-width: 0px;
+ &:not(.off) {
+ &.in-range {
+ background-color: gray('200');
+ }
+ &.available:hover {
+ background-color: gray('200');
+ }
+ &.active, &.active:hover {
+ background-color: $o-brand-primary;
+ }
+ }
+ &.off:hover {
+ background: #FFFFFF;
+ color: #999;
+ }
+ }
+ }
+ }
+ }
+ .calendar-time {
+ select {
+ &.hourselect, &.minuteselect, &.secondselect, &.ampmselect {
+ display: initial;
+ -webkit-appearance: menulist-button;
+ -moz-appearance: menulist-button;
+ appearance: menulist-button;
+ }
+ }
+ }
+ }
+ .drp-buttons {
+ .drp-selected {
+ display: none;
+ }
+ }
+}
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 3699142971e..8d5dde14bfe 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -2578,6 +2578,174 @@ QUnit.module('basic_fields', {
});
+ QUnit.module('FieldDateRange');
+
+ QUnit.test('Datetime field', async function (assert) {
+ assert.expect(19);
+
+ this.data.partner.fields.datetime_end = {string: 'Datetime End', type: 'datetime'};
+ this.data.partner.records[0].datetime_end = '2017-03-13 00:00:00';
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ session: {
+ getTZOffset: function () {
+ return 330;
+ },
+ },
+ });
+
+ // Check date display correctly in readonly
+ assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/08/2017 15:30:00',
+ "the start date should be correctly displayed in readonly");
+ assert.strictEqual(form.$('.o_field_date_range:last').text(), '03/13/2017 05:30:00',
+ "the end date should be correctly displayed in readonly");
+
+ // Edit
+ await testUtils.form.clickEdit(form);
+
+ // Check date range picker initialization
+ assert.containsN(document.body, '.daterangepicker', 2,
+ "should initialize 2 date range picker");
+ assert.strictEqual($('.daterangepicker:first').css('display'), 'block',
+ "date range picker should be opened initially");
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'none',
+ "date range picker should be closed initially");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.left .active.start-date').text(), '8',
+ "active start date should be '8' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.left .hourselect').val(), '15',
+ "active start date hour should be '15' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.left .minuteselect').val(), '30',
+ "active start date minute should be '30' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.right .active.end-date').text(), '13',
+ "active end date should be '13' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.right .hourselect').val(), '5',
+ "active end date hour should be '5' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.right .minuteselect').val(), '30',
+ "active end date minute should be '30' in date range picker");
+
+ // Close picker
+ await testUtils.dom.click($('.daterangepicker:first .cancelBtn'));
+ assert.strictEqual($('.daterangepicker:first').css('display'), 'none',
+ "date range picker should be closed");
+
+ // Try to check with end date
+ await testUtils.dom.click(form.$('.o_field_date_range:last'));
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'block',
+ "date range picker should be opened");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.left .active.start-date').text(), '8',
+ "active start date should be '8' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.left .hourselect').val(), '15',
+ "active start date hour should be '15' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.left .minuteselect').val(), '30',
+ "active start date minute should be '30' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.right .active.end-date').text(), '13',
+ "active end date should be '13' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.right .hourselect').val(), '5',
+ "active end date hour should be '5' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.right .minuteselect').val(), '30',
+ "active end date minute should be '30' in date range picker");
+
+ form.destroy();
+ });
+
+ QUnit.test('Date field', async function (assert) {
+ assert.expect(18);
+
+ this.data.partner.fields.date_end = {string: 'Date End', type: 'date'};
+ this.data.partner.records[0].date_end = '2017-02-08';
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ session: {
+ getTZOffset: function () {
+ return 330;
+ },
+ },
+ });
+
+ // Check date display correctly in readonly
+ assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/03/2017',
+ "the start date should be correctly displayed in readonly");
+ assert.strictEqual(form.$('.o_field_date_range:last').text(), '02/08/2017',
+ "the end date should be correctly displayed in readonly");
+
+ // Edit
+ await testUtils.form.clickEdit(form);
+
+ // Check date range picker initialization
+ assert.containsN(document.body, '.daterangepicker', 2,
+ "should initialize 2 date range picker");
+ assert.strictEqual($('.daterangepicker:first').css('display'), 'block',
+ "date range picker should be opened initially");
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'none',
+ "date range picker should be closed initially");
+ assert.strictEqual($('.daterangepicker:first .active.start-date').text(), '3',
+ "active start date should be '3' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .active.end-date').text(), '8',
+ "active end date should be '8' in date range picker");
+
+ // Change date
+ await testUtils.dom.triggerMouseEvent($('.daterangepicker:first .drp-calendar.left .available:contains("16")'), 'mousedown');
+ await testUtils.dom.triggerMouseEvent($('.daterangepicker:first .drp-calendar.right .available:contains("12")'), 'mousedown');
+ await testUtils.dom.click($('.daterangepicker:first .applyBtn'));
+
+ // Check date after change
+ assert.strictEqual($('.daterangepicker:first').css('display'), 'none',
+ "date range picker should be closed");
+ assert.strictEqual(form.$('.o_field_date_range:first').val(), '02/16/2017',
+ "the date should be '02/16/2017'");
+ assert.strictEqual(form.$('.o_field_date_range:last').val(), '03/12/2017',
+ "'the date should be '03/12/2017'");
+
+ // Try to change range with end date
+ await testUtils.dom.click(form.$('.o_field_date_range:last'));
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'block',
+ "date range picker should be opened");
+ assert.strictEqual($('.daterangepicker:last .active.start-date').text(), '16',
+ "start date should be a 16 in date range picker");
+ assert.strictEqual($('.daterangepicker:last .active.end-date').text(), '12',
+ "end date should be a 12 in date range picker");
+
+ // Change date
+ await testUtils.dom.triggerMouseEvent($('.daterangepicker:last .drp-calendar.left .available:contains("13")'), 'mousedown');
+ await testUtils.dom.triggerMouseEvent($('.daterangepicker:last .drp-calendar.right .available:contains("18")'), 'mousedown');
+ await testUtils.dom.click($('.daterangepicker:last .applyBtn'));
+
+ // Check date after change
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'none',
+ "date range picker should be closed");
+ assert.strictEqual(form.$('.o_field_date_range:first').val(), '02/13/2017',
+ "the start date should be '02/13/2017'");
+ assert.strictEqual(form.$('.o_field_date_range:last').val(), '03/18/2017',
+ "the end date should be '03/18/2017'");
+
+ // Save
+ await testUtils.form.clickSave(form);
+
+ // Check date after save
+ assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/13/2017',
+ "the start date should be '02/13/2017' after save");
+ assert.strictEqual(form.$('.o_field_date_range:last').text(), '03/18/2017',
+ "the end date should be '03/18/2017' after save");
+
+ form.destroy();
+ });
+
QUnit.module('FieldDate');
QUnit.test('date field: toggle datepicker [REQUIRE FOCUS]', async function (assert) {
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index fc805284ffc..6a225d7a636 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -72,6 +72,7 @@
+
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index 6cf2eb9430c..5e03f9346fa 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -1651,6 +1651,23 @@ order.
+- daterange (FieldDateRange)
+ This widget allow user to select start and end date into single picker.
+
+ - Supported field types: *date*, *datetime*
+
+ Options:
+
+ - related_start_date: apply on end date field to get start date value which
+ is used to display range in the picker.
+ - related_end_date: apply on start date field to get end date value which
+ is used to display range in the picker.
+ - picker_options: extra settings for picker.
+
+ .. code-block:: xml
+
+
+
- monetary (FieldMonetary)
This is the default field type for fields of type 'monetary'. It is used to
display a currency. If there is a currency fields given in option, it will