Example #1
1
odoo.define('web.CalendarRenderer', function (require) {
"use strict";

var AbstractRenderer = require('web.AbstractRenderer');
var config = require('web.config');
var core = require('web.core');
var Dialog = require('web.Dialog');
var field_utils = require('web.field_utils');
var FieldManagerMixin = require('web.FieldManagerMixin');
var QWeb = require('web.QWeb');
var relational_fields = require('web.relational_fields');
var session = require('web.session');
var utils = require('web.utils');
var Widget = require('web.Widget');

var _t = core._t;
var qweb = core.qweb;

var scales = {
    day: 'agendaDay',
    week: 'agendaWeek',
    month: 'month'
};

var SidebarFilterM2O = relational_fields.FieldMany2One.extend({
    _getSearchBlacklist: function () {
        return this._super.apply(this, arguments).concat(this.filter_ids || []);
    },
});

var SidebarFilter = Widget.extend(FieldManagerMixin, {
    template: 'CalendarView.sidebar.filter',
    custom_events: _.extend({}, FieldManagerMixin.custom_events, {
        field_changed: '_onFieldChanged',
    }),
    /**
     * @constructor
     * @param {Widget} parent
     * @param {Object} options
     * @param {string} options.fieldName
     * @param {Object[]} options.filters A filter is an object with the
     *   following keys: id, value, label, active, avatar_model, color,
     *   can_be_removed
     * @param {Object} [options.favorite] this is an object with the following
     *   keys: fieldName, model, fieldModel
     */
    init: function (parent, options) {
        this._super.apply(this, arguments);
        FieldManagerMixin.init.call(this);

        this.title = options.title;
        this.fields = options.fields;
        this.fieldName = options.fieldName;
        this.write_model = options.write_model;
        this.write_field = options.write_field;
        this.avatar_field = options.avatar_field;
        this.avatar_model = options.avatar_model;
        this.filters = options.filters;
        this.label = options.label;
        this.getColor = options.getColor;
    },
    /**
     * @override
     */
    willStart: function () {
        var self = this;
        var defs = [this._super.apply(this, arguments)];

        if (this.write_model || this.write_field) {
            var def = this.model.makeRecord(this.write_model, [{
                name: this.write_field,
                relation: this.fields[this.fieldName].relation,
                type: 'many2one',
            }]).then(function (recordID) {
                self.many2one = new SidebarFilterM2O(self,
                    self.write_field,
                    self.model.get(recordID),
                    {
                        mode: 'edit',
                        can_create: false,
                        attrs: {
                            'placeholder': _.str.sprintf(_t("Add %s"), self.title),
                        },
                    });
            });
            defs.push(def);
        }
        return $.when.apply($, defs);

    },
    /**
     * @override
     */
    start: function () {
        this._super();
        if (this.many2one) {
            this.many2one.appendTo(this.$el);
            this.many2one.filter_ids = _.without(_.pluck(this.filters, 'value'), 'all');
        }
        this.$el.on('click', '.o_remove', this._onFilterRemove.bind(this));
        this.$el.on('click', '.custom-checkbox input', this._onFilterActive.bind(this));
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @private
     * @param {OdooEvent} event
     */
    _onFieldChanged: function (event) {
        var self = this;
        event.stopPropagation();
        var value = event.data.changes[this.write_field].id;
        this._rpc({
                model: this.write_model,
                method: 'create',
                args: [{'user_id': session.uid,'partner_id': value,}],
            })
            .then(function () {
                self.trigger_up('changeFilter', {
                    'fieldName': self.fieldName,
                    'value': value,
                    'active': true,
                });
            });
    },
    /**
     * @private
     * @param {MouseEvent} e
     */
    _onFilterActive: function (e) {
        var $input = $(e.currentTarget);
        this.trigger_up('changeFilter', {
            'fieldName': this.fieldName,
            'value': $input.closest('.o_calendar_filter_item').data('value'),
            'active': $input.prop('checked'),
        });
    },
    /**
     * @private
     * @param {MouseEvent} e
     */
    _onFilterRemove: function (e) {
        var self = this;
        var $filter = $(e.currentTarget).closest('.o_calendar_filter_item');
        Dialog.confirm(this, _t("Do you really want to delete this filter from favorites ?"), {
            confirm_callback: function () {
                self._rpc({
                        model: self.write_model,
                        method: 'unlink',
                        args: [[$filter.data('id')]],
                    })
                    .then(function () {
                        self.trigger_up('changeFilter', {
                            'fieldName': self.fieldName,
                            'id': $filter.data('id'),
                            'active': false,
                            'value': $filter.data('value'),
                        });
                    });
            },
        });
    },
});

return AbstractRenderer.extend({
    template: "CalendarView",
    events: _.extend({}, AbstractRenderer.prototype.events, {
        'click .o_calendar_sidebar_toggler': '_onToggleSidebar',
    }),

    /**
     * @constructor
     * @param {Widget} parent
     * @param {Object} state
     * @param {Object} params
     */
    init: function (parent, state, params) {
        this._super.apply(this, arguments);
        this.displayFields = params.displayFields;
        this.model = params.model;
        this.filters = [];
        this.color_map = {};

        if (params.eventTemplate) {
            this.qweb = new QWeb(session.debug, {_s: session.origin});
            this.qweb.add_template(utils.json_node_to_xml(params.eventTemplate));
        }
    },
    /**
     * @override
     * @returns {Deferred}
     */
    start: function () {
        this._initSidebar();
        this._initCalendar();
        if (config.device.isMobile) {
            this._bindSwipe();
        }
        return this._super();
    },
    /**
     * @override
     */
    destroy: function () {
        if (this.$calendar) {
            this.$calendar.fullCalendar('destroy');
        }
        if (this.$small_calendar) {
            this.$small_calendar.datepicker('destroy');
            $('#ui-datepicker-div:empty').remove();
        }
        this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * Note: this is not dead code, it is called by the calendar-box template
     *
     * @param {any} record
     * @param {any} fieldName
     * @param {any} imageField
     * @returns {string[]}
     */
    getAvatars: function (record, fieldName, imageField) {
        var field = this.state.fields[fieldName];

        if (!record[fieldName]) {
            return [];
        }
        if (field.type === 'one2many' || field.type === 'many2many') {
            return _.map(record[fieldName], function (id) {
                return '<img src="/web/image/'+field.relation+'/'+id+'/'+imageField+'" />';
            });
        } else if (field.type === 'many2one') {
            return ['<img src="/web/image/'+field.relation+'/'+record[fieldName][0]+'/'+imageField+'" />'];
        } else {
            var value = this._format(record, fieldName);
            var color = this.getColor(value);
            if (isNaN(color)) {
                return ['<span class="o_avatar_square" style="background-color:'+color+';"/>'];
            }
            else {
                return ['<span class="o_avatar_square o_calendar_color_'+color+'"/>'];
            }
        }
    },
    /**
     * Note: this is not dead code, it is called by two template
     *
     * @param {any} key
     * @returns {integer}
     */
    getColor: function (key) {
        if (!key) {
            return;
        }
        if (this.color_map[key]) {
            return this.color_map[key];
        }
        // check if the key is a css color
        if (typeof key === 'string' && key.match(/^((#[A-F0-9]{3})|(#[A-F0-9]{6})|((hsl|rgb)a?\(\s*(?:(\s*\d{1,3}%?\s*),?){3}(\s*,[0-9.]{1,4})?\))|)$/i)) {
            return this.color_map[key] = key;
        }
        var index = (((_.keys(this.color_map).length + 1) * 5) % 24) + 1;
        this.color_map[key] = index;
        return index;
    },
    /**
     * @override
     */
    getLocalState: function () {
        var $fcScroller = this.$calendar.find('.fc-scroller');
        return {
            scrollPosition: $fcScroller.scrollTop(),
        };
    },
    /**
     * @override
     */
    setLocalState: function (localState) {
        if (localState.scrollPosition) {
            var $fcScroller = this.$calendar.find('.fc-scroller');
            $fcScroller.scrollTop(localState.scrollPosition);
        }
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * @private
     * Bind handlers to enable swipe navigation
     *
     * @private
     */
    _bindSwipe: function () {
        var self = this;
        var touchStartX;
        var touchEndX;
        this.$calendar.on('touchstart', function (event) {
            touchStartX = event.originalEvent.touches[0].pageX;
        });
        this.$calendar.on('touchend', function (event) {
            touchEndX = event.originalEvent.changedTouches[0].pageX;
            if (touchStartX - touchEndX > 100) {
                self.trigger_up('next');
            } else if (touchStartX - touchEndX < -100) {
                self.trigger_up('prev');
            }
        });
    },
    /**
     * @param {any} event
     * @returns {string} the html for the rendered event
     */
    _eventRender: function (event) {
        var qweb_context = {
            event: event,
            fields: this.state.fields,
            format: this._format.bind(this),
            isMobile: config.device.isMobile,
            read_only_mode: this.read_only_mode,
            record: event.record,
            user_context: session.user_context,
            widget: this,
        };
        this.qweb_context = qweb_context;
        if (_.isEmpty(qweb_context.record)) {
            return '';
        } else {
            return (this.qweb || qweb).render("calendar-box", qweb_context);
        }
    },
    /**
     * @private
     * @param {any} record
     * @param {any} fieldName
     * @returns {string}
     */
    _format: function (record, fieldName) {
        var field = this.state.fields[fieldName];
        if (field.type === "one2many" || field.type === "many2many") {
            return field_utils.format[field.type]({data: record[fieldName]}, field);
        } else {
            return field_utils.format[field.type](record[fieldName], field, {forceString: true});
        }
    },
    /**
     * Initialize the main calendar
     *
     * @private
     */
    _initCalendar: function () {
        var self = this;

        this.$calendar = this.$(".o_calendar_widget");

        //Documentation here : http://arshaw.com/fullcalendar/docs/
        var fc_options = $.extend({}, this.state.fc_options, {
            eventDrop: function (event) {
                self.trigger_up('dropRecord', event);
            },
            eventResize: function (event) {
                self.trigger_up('updateRecord', event);
            },
            eventClick: function (event) {
                self.trigger_up('openEvent', event);
                self.$calendar.fullCalendar('unselect');
            },
            select: function (target_date, end_date, event, _js_event, _view) {
                var data = {'start': target_date, 'end': end_date};
                if (self.state.context.default_name) {
                    data.title = self.state.context.default_name;
                }
                self.trigger_up('openCreate', data);
                self.$calendar.fullCalendar('unselect');
            },
            eventRender: function (event, element) {
                var $render = $(self._eventRender(event));
                event.title = $render.find('.o_field_type_char:first').text();
                element.find('.fc-content').html($render.html());
                element.addClass($render.attr('class'));
                var display_hour = '';
                if (!event.allDay) {
                    var start = event.r_start || event.start;
                    var end = event.r_end || event.end;
                    var timeFormat = _t.database.parameters.time_format.search("%H") != -1 ? 'HH:mm': 'h:mma';
                    display_hour = start.format(timeFormat) + ' - ' + end.format(timeFormat);
                    if (display_hour === '00:00 - 00:00') {
                        display_hour = _t('All day');
                    }
                }
                element.find('.fc-content .fc-time').text(display_hour);
            },
            // Dirty hack to ensure a correct first render
            eventAfterAllRender: function () {
                $(window).trigger('resize');
            },
            viewRender: function (view) {
                // compute mode from view.name which is either 'month', 'agendaWeek' or 'agendaDay'
                var mode = view.name === 'month' ? 'month' : (view.name === 'agendaWeek' ? 'week' : 'day');
                self.trigger_up('viewUpdated', {
                    mode: mode,
                    title: view.title,
                });
            },
            height: 'parent',
            unselectAuto: false,
        });

        this.$calendar.fullCalendar(fc_options);
    },
    /**
     * Initialize the mini calendar in the sidebar
     *
     * @private
     */
    _initCalendarMini: function () {
        var self = this;
        this.$small_calendar = this.$(".o_calendar_mini");
        this.$small_calendar.datepicker({
            'onSelect': function (datum, obj) {
                self.trigger_up('changeDate', {
                    date: moment(new Date(+obj.currentYear , +obj.currentMonth, +obj.currentDay))
                });
            },
            'dayNamesMin' : this.state.fc_options.dayNamesShort,
            'monthNames': this.state.fc_options.monthNamesShort,
            'firstDay': this.state.fc_options.firstDay,
        });
    },
    /**
     * Initialize the sidebar
     *
     * @private
     */
    _initSidebar: function () {
        this.$sidebar = this.$('.o_calendar_sidebar');
        this.$sidebar_container = this.$(".o_calendar_sidebar_container");
        this._initCalendarMini();
    },
    /**
     * Render the calendar view, this is the main entry point.
     *
     * @override method from AbstractRenderer
     * @private
     * @returns {Deferred}
     */
    _render: function () {
        var $calendar = this.$calendar;
        var $fc_view = $calendar.find('.fc-view');
        var scrollPosition = $fc_view.scrollLeft();
        var scrollTop = this.$calendar.find('.fc-scroller').scrollTop();

        $fc_view.scrollLeft(0);
        $calendar.fullCalendar('unselect');

        if (scales[this.state.scale] !== $calendar.data('fullCalendar').getView().type) {
            $calendar.fullCalendar('changeView', scales[this.state.scale]);
        }

        if (this.target_date !== this.state.target_date.toString()) {
            $calendar.fullCalendar('gotoDate', moment(this.state.target_date));
            this.target_date = this.state.target_date.toString();
        }

        this.$small_calendar.datepicker("setDate", this.state.highlight_date.toDate())
                            .find('.o_selected_range')
                            .removeClass('o_color o_selected_range');
        var $a;
        switch (this.state.scale) {
            case 'month': $a = this.$small_calendar.find('td a'); break;
            case 'week': $a = this.$small_calendar.find('tr:has(.ui-state-active) a'); break;
            case 'day': $a = this.$small_calendar.find('a.ui-state-active'); break;
        }
        $a.addClass('o_selected_range');
        setTimeout(function () {
            $a.not('.ui-state-active').addClass('o_color');
        });

        $fc_view.scrollLeft(scrollPosition);

        var fullWidth = this.state.fullWidth;
        this.$('.o_calendar_sidebar_toggler')
            .toggleClass('fa-close', !fullWidth)
            .toggleClass('fa-chevron-left', fullWidth)
            .attr('title', !fullWidth ? _('Close Sidebar') : _('Open Sidebar'));
        this.$sidebar_container.toggleClass('o_sidebar_hidden', fullWidth);
        this.$sidebar.toggleClass('o_hidden', fullWidth);

        this._renderFilters();
        this.$calendar.appendTo('body');
        if (scrollTop) {
            this.$calendar.fullCalendar('reinitView');
        } else {
            this.$calendar.fullCalendar('render');
        }
        this._renderEvents();
        this.$calendar.prependTo(this.$('.o_calendar_view'));

        return this._super.apply(this, arguments);
    },
    /**
     * Render all events
     *
     * @private
     */
    _renderEvents: function () {
        this.$calendar.fullCalendar('removeEvents');
        this.$calendar.fullCalendar('addEventSource', this.state.data);
    },
    /**
     * Render all filters
     *
     * @private
     */
    _renderFilters: function () {
        var self = this;
        _.each(this.filters || (this.filters = []), function (filter) {
            filter.destroy();
        });
        if (this.state.fullWidth) {
            return;
        }
        _.each(this.state.filters, function (options) {
            if (!_.find(options.filters, function (f) {return f.display == null || f.display;})) {
                return;
            }
            options.getColor = self.getColor.bind(self);
            options.fields = self.state.fields;
            var filter = new SidebarFilter(self, options);
            filter.appendTo(self.$sidebar);
            self.filters.push(filter);
        });
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * Toggle the sidebar
     *
     * @private
     */
    _onToggleSidebar: function () {
        this.trigger_up('toggleFullWidth');
    },
});

});
Example #2
0
odoo.define('web.special_fields', function (require) {
"use strict";

var core = require('web.core');
var field_utils = require('web.field_utils');
var relational_fields = require('web.relational_fields');

var FieldSelection = relational_fields.FieldSelection;
var _t = core._t;


/**
 * This widget is intended to display a warning near a label of a 'timezone' field
 * indicating if the browser timezone is identical (or not) to the selected timezone.
 * This widget depends on a field given with the param 'tz_offset_field', which contains
 * the time difference between UTC time and local time, in minutes.
 */
var FieldTimezoneMismatch = FieldSelection.extend({
    /**
     * @override
     */
    start: function () {
        var interval = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ? 60000 : 1000;
        this._datetime = setInterval(this._renderDateTimeTimezone.bind(this), interval);
        return this._super.apply(this, arguments);
    },
    /**
     * @override
     */
    destroy: function () {
        clearInterval(this._datetime);
        return this._super();
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * @override
     * @private
     */
    _render: function () {
        this._super.apply(this, arguments);
        this._renderTimezoneMismatch();
    },
    /**
     * Display the time in the user timezone (reload each second)
     *
     * @private
     */
    _renderDateTimeTimezone: function () {
        if (!this.mismatch) {
            return;
        }
        var offset = this.recordData.tz_offset.match(/([+-])([0-9]{2})([0-9]{2})/);
        offset = (offset[1] === '-' ? -1 : 1) * (parseInt(offset[2])*60 + parseInt(offset[3]));
        var datetime = field_utils.format.datetime(moment.utc().add(offset, 'minutes'), this.field, {timezone: false});
        var content = this.$option.html().split(' ')[0];
        content += '    ('+ datetime + ')';
        this.$option.html(content);
    },
    /**
     * Display the timezone alert
     * 
     * Note: timezone alert is a span that is added after $el, and $el is now a
     * set of two elements
     *
     * @private
     */
    _renderTimezoneMismatch: function () {
        // we need to clean the warning to have maximum one alert
        this.$el.last().filter('.o_tz_warning').remove();
        this.$el = this.$el.first();
        var value = this.$el.val();

        if (this.$option) {
            this.$option.html(this.$option.html().split(' ')[0]);
        }

        var userOffset = this.recordData.tz_offset;
        this.mismatch = false;
        if (userOffset && value !== "" && value !== "false") {
            var offset = -(new Date().getTimezoneOffset());
            var browserOffset = (offset < 0) ? "-" : "+";
            browserOffset += _.str.sprintf("%02d", Math.abs(offset / 60));
            browserOffset += _.str.sprintf("%02d", Math.abs(offset % 60));
            this.mismatch = (browserOffset !== userOffset);
        }

        if (this.mismatch){
            var $span = $('<span class="fa fa-exclamation-triangle o_tz_warning"/>');
            $span.insertAfter(this.$el);
            $span.attr('title', _t("Timezone Mismatch : The timezone of your browser doesn't match the selected one. The time in Odoo is displayed according to the timezone set on your user's preferences."));
            this.$el = this.$el.add($span);

            this.$option = this.$('option').filter(function () {
                return $(this).attr('value') === value;
            });
            this._renderDateTimeTimezone();
        }
    },
    /**
     * @override
     * @private
     * this.$el can have other elements than select
     * that should not be touched
     */
    _renderEdit: function () {
        // FIXME: hack to handle multiple root elements
        // in this.$el , which is a bad idea
        // In master we should make this.$el a wrapper
        // around multiple subelements
        var $otherEl = this.$el.not('select');
        this.$el = this.$el.first();

        this._super.apply(this, arguments);

        $otherEl.insertAfter(this.$el);
        this.$el = this.$el.add($otherEl);
    },
});

var FieldReportLayout = relational_fields.FieldMany2One.extend({
    // this widget is not generic, so we disable its studio use
    // supportedFieldTypes: ['many2one', 'selection'],
    events: _.extend({}, relational_fields.FieldMany2One.prototype.events, {
        'click img': '_onImgClicked',
    }),

    willStart: function () {
        var self = this;
        this.previews = {};
        return this._super()
            .then(function () {
                return self._rpc({
                    model: 'report.layout',
                    method: "search_read"
                }).then(function (values) {
                    self.previews = values;
                });
            });
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * @override
     * @private
     */
    _render: function () {
        var self = this;
        this.$el.empty();
        var value = _.isObject(this.value) ? this.value.data.id : this.value;
        _.each(this.previews, function (val) {
            var $container = $('<div>').addClass('col-3 text-center');
            var $img = $('<img>')
                .addClass('img img-fluid img-thumbnail ml16')
                .toggleClass('btn-info', val.view_id[0] === value)
                .attr('src', val.image)
                .data('key', val.view_id[0]);
            $container.append($img);
            if (val.pdf) {
                var $previewLink = $('<a>')
                    .text('Example')
                    .attr('href', val.pdf)
                    .attr('target', '_blank');
                $container.append($previewLink);
            }
            self.$el.append($container);
        });
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @override
     * @private
     * @param {MouseEvent} event
     */
    _onImgClicked: function (event) {
        this._setValue($(event.currentTarget).data('key'));
    },
});


return {
    FieldTimezoneMismatch: FieldTimezoneMismatch,
    FieldReportLayout: FieldReportLayout,
};

});
odoo.define('sale_product_configurator.product_configurator', function (require) {
var relationalFields = require('web.relational_fields');
var FieldsRegistry = require('web.field_registry');
var core = require('web.core');
var _t = core._t;

/**
 * The product configurator widget is a simple FieldMany2One that adds the capability
 * to configure a product_template_id using the product configurator wizard.
 *
 * !!! It should only be used on a product_template_id field !!!
 */
var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
    events: _.extend({}, relationalFields.FieldMany2One.prototype.events, {
        'click .o_edit_product_configuration': '_onEditProductConfiguration'
    }),

    /**
     * This method will check if the current product_template set on the SO line is configurable
     * -> If so, we add a 'Edit Configuration' button next to the dropdown.
     *
     * @override
     */
    start: function () {
        var prom = this._super.apply(this, arguments);

        var $inputDropdown = this.$('.o_input_dropdown');

        if (this.recordData.is_configurable_product &&
            $inputDropdown.length !== 0 &&
            this.$('.o_edit_product_configuration').length === 0) {
            var $editConfigurationButton = $('<button>', {
                type: 'button',
                class: 'fa fa-pencil btn btn-secondary o_edit_product_configuration',
                tabindex: '-1',
                draggable: false,
                'aria-label': _t('Edit Configuration'),
                title: _t('Edit Configuration')
            });

            $inputDropdown.after($editConfigurationButton);
        }
        return prom;
    },

    /**
     * This method is overridden to check to check if the product_template_id
     * needs configuration or not:
     *
     * - The product_template has only one "product.product" and is not dynamic
     *   -> Set the product_id on the SO line
     *   -> If the product has optional products, open the configurator in 'options' mode
     *
     * - The product_template is configurable
     *   -> Open the product configurator wizard and initialize it with
     *      the provided product_template_id and its current attribute values
     * @override
     * @param {OdooEvent} event
     *   {boolean} event.data.preventProductIdCheck prevent the product configurator widget
     *     from looping forever when it needs to change the 'product_template_id'
     *
     * @private
     */
    _onFieldChanged: function (event) {
        var self = this;
        self.restoreProductTemplateId = self.recordData.product_template_id;

        this._super.apply(this, arguments);

        var $inputDropdown = this.$('.o_input_dropdown');
        if (event.data.changes.product_template_id
            && $inputDropdown.length !== 0 &&
            this.$('.o_edit_product_configuration').length !== 0){
            this.$('.o_edit_product_configuration').remove();
        }

        if (!event.data.changes.product_template_id
            || event.data.preventProductIdCheck){
            return;
        }

        var productTemplateId = event.data.changes.product_template_id.id;
        if (productTemplateId){
            this._rpc({
                model: 'product.template',
                method: 'get_single_product_variant',
                args: [
                    productTemplateId
                ]
            }).then(function (result){
                if (result){
                    self.trigger_up('field_changed', {
                        dataPointID: event.data.dataPointID,
                        changes: {
                            product_id: {id: result.product_id},
                            product_custom_attribute_value_ids: {
                                operation: 'DELETE_ALL'
                            }
                        },
                        onSuccess: function () {
                            if (result.has_optional_products) {
                                self._openProductConfigurator({
                                        configuratorMode: 'options',
                                        default_pricelist_id: self._getPricelistId(),
                                        default_product_template_id: productTemplateId
                                    },
                                    event.data.dataPointID
                                );
                            }
                        }
                    });

                    self._onSimpleProductFound(result.product_id, event.data.dataPointID);
                } else {
                    self._openProductConfigurator({
                            configuratorMode: 'add',
                            default_pricelist_id: self._getPricelistId(),
                            default_product_template_id: productTemplateId
                        },
                        event.data.dataPointID
                    );
                }
            });
        }
    },

    /**
     * Hooking point for other modules
     *
     * @param {integer} productId
     * @param {string} dataPointID
     *
     * @private
     */
    _onSimpleProductFound: function (productId, dataPointID) {},

    /**
     * Opens the product configurator to allow configuring the product template
     * and its various options.
     *
     * The configuratorMode param controls how to open the configurator.
     * - The "add" mode will allow configuring the product template & options.
     * - The "edit" mode will only allow editing the product template's configuration.
     * - The "options" mode is a special case where the product configurator is used as a bridge
     *   between the SO line and the optional products modal. It will hide its window and handle
     *   the communication between those two.
     *
     * When the configuration is canceled (i.e when the product configurator is closed using the
     * "CANCEL" button or the cross on the top right corner of the window),
     * the product_template is reset to its previous value if any.
     *
     * @param {Object} data various "default_" values
     *  {string} data.configuratorMode 'add' or 'edit'.
     * @param {string} dataPointId
     *
     * @private
     */
    _openProductConfigurator: function (data, dataPointId) {
        var self = this;
        this.do_action('sale_product_configurator.sale_product_configurator_action', {
            additional_context: data,
            on_close: function (result) {
                if (result && result !== 'special'){
                    self._addProducts(result, dataPointId);
                } else {
                    if (self.restoreProductTemplateId) {
                        self.trigger_up('field_changed', {
                            dataPointID: dataPointId,
                            preventProductIdCheck: true,
                            changes: {
                                product_template_id: self.restoreProductTemplateId.data
                            }
                        });
                    }
                }
            }
        });
    },

    /**
     * Opens the product configurator in "edit" mode.
     * (see '_openProductConfigurator' for more info on the "edit" mode).
     * The requires to retrieve all the needed data from the SO line
     * that are kept in the "recordData" object.
     *
     * @private
     */
    _onEditProductConfiguration: function () {
        this._openProductConfigurator({
                configuratorMode: 'edit',
                default_product_template_id: this.recordData.product_template_id.data.id,
                default_pricelist_id: this._getPricelistId(),
                default_product_template_attribute_value_ids: this._convertFromMany2Many(
                    this.recordData.product_template_attribute_value_ids
                ),
                default_product_no_variant_attribute_value_ids: this._convertFromMany2Many(
                    this.recordData.product_no_variant_attribute_value_ids
                ),
                default_product_custom_attribute_value_ids: this._convertFromOne2Many(
                    this.recordData.product_custom_attribute_value_ids
                ),
                default_quantity: this.recordData.product_uom_qty
            },
            this.dataPointID
        );
    },

    /**
     * This will first modify the SO line to update all the information coming from
     * the product configurator using the 'field_changed' event.
     *
     * onSuccess from that first method, it will add the optional products to the SO
     * using the 'add_record' event.
     *
     * Doing both at the same time could lead to unordered product_template/options.
     *
     * @param {Object} products the products to add to the SO line.
     *   {Object} products.mainProduct the product_template configured
     *     with various attribute/custom values
     *   {Array} products.options the various selected optional products
     *     with their configuration
     * @param {string} dataPointId
     *
     * @private
     */
    _addProducts: function (result, dataPointId) {
        var self = this;
        this.trigger_up('field_changed', {
            dataPointID: dataPointId,
            preventProductIdCheck: true,
            changes: this._getMainProductChanges(result.mainProduct),
            onSuccess: function () {
                if (result.options) {
                    var parentList = self.getParent();
                    self.trigger_up('add_record', {
                        context: self._productsToRecords(result.options),
                        forceEditable: 'bottom',
                        allowWarning: true,
                        onSuccess: function (){
                            parentList.unselectRow();
                        }
                    });
                }
            }
        });
    },

    /**
     * This will convert the result of the product configurator into
     * "changes" that are understood by the basic_model.js
     *
     * For the product_custom_attribute_value_ids, we need to do a DELETE_ALL
     * command to clean the currently selected values and then a CREATE for every
     * custom value specified in the configurator.
     *
     * For the product_no_variant_attribute_value_ids, we also need to do a DELETE_ALL
     * command to clean the currently selected values and issue a single ADD_M2M containing
     * all the ids of the product_attribute_values.
     *
     * @param {Object} mainProduct
     *
     * @private
     */
    _getMainProductChanges: function (mainProduct) {
        var result = {
            product_id: {id: mainProduct.product_id},
            product_template_id: {id: mainProduct.product_template_id},
            product_uom_qty: mainProduct.quantity
        };

        var customAttributeValues = mainProduct.product_custom_attribute_values;
        var customValuesCommands = [{operation: 'DELETE_ALL'}];
        if (customAttributeValues && customAttributeValues.length !== 0) {
            _.each(customAttributeValues, function (customValue) {
                // FIXME awa: This could be optimized by adding a "disableDefaultGet" to avoid
                // having multiple default_get calls that are useless since we already
                // have all the default values locally.
                // However, this would mean a lot of changes in basic_model.js to handle
                // those "default_" values and set them on the various fields (text,o2m,m2m,...).
                // -> This is not considered as worth it right now.
                customValuesCommands.push({
                    operation: 'CREATE',
                    context: [{
                        default_attribute_value_id: customValue.attribute_value_id,
                        default_custom_value: customValue.custom_value
                    }]
                });
            });
        }

        result['product_custom_attribute_value_ids'] = {
            operation: 'MULTI',
            commands: customValuesCommands
        };

        var noVariantAttributeValues = mainProduct.no_variant_attribute_values;
        var noVariantCommands = [{operation: 'DELETE_ALL'}];
        if (noVariantAttributeValues && noVariantAttributeValues.length !== 0) {
            var res_ids = _.map(noVariantAttributeValues, function (noVariantValue) {
                return {id: parseInt(noVariantValue.value)};
            });

            noVariantCommands.push({
                operation: 'ADD_M2M',
                ids: res_ids
            });
        }

        result['product_no_variant_attribute_value_ids'] = {
            operation: 'MULTI',
            commands: noVariantCommands
        };

        return result;
    },

    /**
     * Returns the pricelist_id set on the sale_order form
     *
     * @private
     * @returns {integer} pricelist_id's id
     */
    _getPricelistId: function () {
        return this.record.evalContext.parent.pricelist_id;
    },

    /**
     * Will map the products to appropriate record objects that are
     * ready for the default_get.
     *
     * @param {Array} products The products to transform into records
     *
     * @private
     */
    _productsToRecords: function (products) {
        var records = [];
        _.each(products, function (product){
            var record = {
                default_product_id: product.product_id,
                default_product_template_id: product.product_template_id,
                default_product_uom_qty: product.quantity
            };

            if (product.no_variant_attribute_values) {
                var default_product_no_variant_attribute_values = [];
                _.each(product.no_variant_attribute_values, function (attribute_value) {
                        default_product_no_variant_attribute_values.push(
                            [4, parseInt(attribute_value.value)]
                        );
                });
                record['default_product_no_variant_attribute_value_ids']
                    = default_product_no_variant_attribute_values;
            }

            if (product.product_custom_attribute_values) {
                var default_custom_attribute_values = [];
                _.each(product.product_custom_attribute_values, function (attribute_value) {
                    default_custom_attribute_values.push(
                            [0, 0, {
                                attribute_value_id: attribute_value.attribute_value_id,
                                custom_value: attribute_value.custom_value
                            }]
                        );
                });
                record['default_product_custom_attribute_value_ids']
                    = default_custom_attribute_values;
            }

            records.push(record);
        });

        return records;
    },

    /**
     * Will convert the values contained in the recordData parameter to
     * a list of '4' operations that can be passed as a 'default_' parameter.
     *
     * @param {Object} recordData
     *
     * @private
     */
    _convertFromMany2Many: function (recordData) {
        if (recordData) {
            var convertedValues = [];
            _.each(recordData.res_ids, function (res_id) {
                convertedValues.push([4, parseInt(res_id)]);
            });

            return convertedValues;
        }

        return null;
    },

    /**
     * Will convert the values contained in the recordData parameter to
     * a list of '0' or '4' operations (based on wether the record is already persisted or not)
     * that can be passed as a 'default_' parameter.
     *
     * @param {Object} recordData
     *
     * @private
     */
    _convertFromOne2Many: function (recordData) {
        if (recordData) {
            var convertedValues = [];
            _.each(recordData.res_ids, function (res_id) {
                if (isNaN(res_id)){
                    _.each(recordData.data, function (record) {
                        if (record.ref === res_id) {
                            convertedValues.push([0, 0, {
                                attribute_value_id: record.data.attribute_value_id.data.id,
                                custom_value: record.data.custom_value
                            }]);
                        }
                    });
                } else {
                    convertedValues.push([4, res_id]);
                }
            });

            return convertedValues;
        }

        return null;
    }
});

FieldsRegistry.add('product_configurator', ProductConfiguratorWidget);

return ProductConfiguratorWidget;

});