Exemplo n.º 1
0
odoo.define('web_editor.backend', function (require) {
'use strict';

var AbstractField = require('web.AbstractField');
var basic_fields = require('web.basic_fields');
var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var field_registry = require('web.field_registry');
var SummernoteManager = require('web_editor.rte.summernote');
var transcoder = require('web_editor.transcoder');

var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin;

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


/**
 * FieldTextHtmlSimple Widget
 * Intended to display HTML content. This widget uses the summernote editor
 * improved by odoo.
 *
 */
var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
    className: 'oe_form_field oe_form_field_html_text',
    supportedFieldTypes: ['html'],

    /**
     * @override
     */
    start: function () {
        new SummernoteManager(this);
        return this._super.apply(this, arguments);
    },

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

    /**
     * Summernote doesn't notify for changes done in code mode. We override
     * commitChanges to manually switch back to normal mode before committing
     * changes, so that the widget is aware of the changes done in code mode.
     *
     * @override
     */
    commitChanges: function () {
        // switch to WYSIWYG mode if currently in code mode to get all changes
        if (config.debug && this.mode === 'edit') {
            var layoutInfo = this.$textarea.data('layoutInfo');
            $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
        }
        this._super.apply(this, arguments);
    },
    /**
     * @override
     */
    isSet: function () {
        return this.value && this.value !== "<p><br/></p>" && this.value.match(/\S/);
    },
    /**
     * Do not re-render this field if it was the origin of the onchange call.
     *
     * @override
     */
    reset: function (record, event) {
        this._reset(record, event);
        if (!event || event.target !== this) {
            if (this.mode === 'edit') {
                this.$content.html(this._textToHtml(this.value));
            } else {
                this._renderReadonly();
            }
        }
        return $.when();
    },

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

    /**
     * @private
     * @returns {Object} the summernote configuration
     */
    _getSummernoteConfig: function () {
        var summernoteConfig = {
            focus: false,
            height: 180,
            toolbar: [
                ['style', ['style']],
                ['font', ['bold', 'italic', 'underline', 'clear']],
                ['fontsize', ['fontsize']],
                ['color', ['color']],
                ['para', ['ul', 'ol', 'paragraph']],
                ['table', ['table']],
                ['insert', ['link', 'picture']],
                ['history', ['undo', 'redo']]
            ],
            prettifyHtml: false,
            styleWithSpan: false,
            inlinemedia: ['p'],
            lang: "odoo",
            onChange: this._doDebouncedAction.bind(this),
        };
        if (config.debug) {
            summernoteConfig.toolbar.splice(7, 0, ['view', ['codeview']]);
        }
        return summernoteConfig;
    },
    /**
     * @override
     * @private
     */
    _getValue: function () {
        if (this.nodeOptions['style-inline']) {
            transcoder.classToStyle(this.$content);
            transcoder.fontToImg(this.$content);
        }
        return this.$content.html();
    },
    /**
     * @override
     * @private
     */
    _renderEdit: function () {
        this.$textarea = $('<textarea>');
        this.$textarea.appendTo(this.$el);
        this.$textarea.summernote(this._getSummernoteConfig());
        this.$content = this.$('.note-editable:first');
        this.$content.html(this._textToHtml(this.value));
        // trigger a mouseup to refresh the editor toolbar
        this.$content.trigger('mouseup');
        if (this.nodeOptions['style-inline']) {
            transcoder.styleToClass(this.$content);
        }
        // reset the history (otherwise clicking on undo before editing the
        // value will empty the editor)
        var history = this.$content.data('NoteHistory');
        if (history) {
            history.reset();
        }
        this.$('.note-toolbar').append(this._renderTranslateButton());
    },
    /**
     * @override
     * @private
     */
    _renderReadonly: function () {
        var self = this;
        this.$el.empty();
        if (this.nodeOptions['style-inline']) {
            var $iframe = $('<iframe class="o_readonly"/>');
            $iframe.on('load', function () {
                self.$content = $($iframe.contents()[0]).find("body");
                self.$content.html(self._textToHtml(self.value));
                self._resize();
            });
            $iframe.appendTo(this.$el);
        } else {
            this.$content = $('<div class="o_readonly"/>');
            this.$content.html(this._textToHtml(this.value));
            this.$content.appendTo(this.$el);
        }
    },
    /**
     * Sets the height of the iframe.
     *
     * @private
     */
    _resize: function () {
        var height = this.$content[0] ? this.$content[0].scrollHeight : 0;
        this.$('iframe').css('height', Math.max(30, Math.min(height, 500)) + 'px');
    },
    /**
     * @private
     * @param {string} text
     * @returns {string} the text converted to html
     */
    _textToHtml: function (text) {
        var value = text || "";
        try {
            $(text)[0].innerHTML; // crashes if text isn't html
        } catch (e) {
            if (value.match(/^\s*$/)) {
                value = '<p><br/></p>';
            } else {
                value = "<p>" + value.split(/<br\/?>/).join("<br/></p><p>") + "</p>";
                value = value
                            .replace(/<p><\/p>/g, '')
                            .replace('<p><p>', '<p>')
                            .replace('<p><p ', '<p ')
                            .replace('</p></p>', '</p>');
            }
        }
        return value;
    },
    /**
     * @override
     * @private
     * @returns {jQueryElement}
     */
    _renderTranslateButton: function () {
        if (_t.database.multi_lang && this.field.translate && this.res_id) {
            return $(QWeb.render('web_editor.FieldTextHtml.button.translate', {widget: this}))
                .on('click', this._onTranslate.bind(this));
        }
        return $();
    },

});

var FieldTextHtml = AbstractField.extend({
    template: 'web_editor.FieldTextHtml',
    supportedFieldTypes: ['html'],

    start: function () {
        var self = this;

        this.loaded = false;
        this.callback = _.uniqueId('FieldTextHtml_');
        window.odoo[this.callback+"_editor"] = function (EditorBar) {
            setTimeout(function () {
                self.on_editor_loaded(EditorBar);
            },0);
        };
        window.odoo[this.callback+"_content"] = function () {
            self.on_content_loaded();
        };
        window.odoo[this.callback+"_updown"] = null;
        window.odoo[this.callback+"_downup"] = function () {
            self.resize();
        };

        // init jqery objects
        this.$iframe = this.$el.find('iframe');
        this.document = null;
        this.$body = $();
        this.$content = $();

        this.$iframe.css('min-height', 'calc(100vh - 360px)');

        // init resize
        this.resize = function resize() {
            if (self.mode === 'edit') {
                if ($("body").hasClass("o_field_widgetTextHtml_fullscreen")) {
                    self.$iframe.css('height', (document.body.clientHeight - self.$iframe.offset().top) + 'px');
                } else {
                    self.$iframe.css("height", (self.$body.find("#oe_snippets").length ? 500 : 300) + "px");
                }
            }
        };
        $(window).on('resize', this.resize);

        this.old_initialize_content();
        var def = this._super.apply(this, arguments);
        return def;
    },
    getDatarecord: function () {
        return this.recordData;
    },
    get_url: function (_attr) {
        var src = this.nodeOptions.editor_url || "/mass_mailing/field/email_template";
        var k;
        var datarecord = this.getDatarecord();
        var attr = {
            'model': this.model,
            'field': this.name,
            'res_id': datarecord.id || '',
            'callback': this.callback
        };
        _attr = _attr || {};

        if (this.nodeOptions['style-inline']) {
            attr.inline_mode = 1;
        }
        if (this.nodeOptions.snippets) {
            attr.snippets = this.nodeOptions.snippets;
        }
        if (this.nodeOptions.template) {
            attr.template = this.nodeOptions.template;
        }
        if (this.mode === "edit") {
            attr.enable_editor = 1;
        }
        if (session.debug) {
            attr.debug = session.debug;
        }

        for (k in _attr) {
            attr[k] = _attr[k];
        }

        if (src.indexOf('?') === -1) {
            src += "?";
        }

        for (k in attr) {
            if (attr[k] !== null) {
                src += "&"+k+"="+(_.isBoolean(attr[k]) ? +attr[k] : attr[k]);
            }
        }

        // delete datarecord[this.name];
        src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord));
        return src;
    },
    old_initialize_content: function () {
        this.$el.closest('.modal-body').css('max-height', 'none');
        this.$iframe = this.$el.find('iframe');
        // deactivate any button to avoid saving a not ready iframe
        $('.o_cp_buttons, .o_statusbar_buttons').find('button').addClass('o_disabled').attr('disabled', true);
        this.document = null;
        this.$body = $();
        this.$content = $();
        this.editor = false;
        window.odoo[this.callback+"_updown"] = null;
        this.$iframe.attr("src", this.get_url());
    },
    on_content_loaded: function () {
        var self = this;
        this.document = this.$iframe.contents()[0];
        this.$body = $("body", this.document);
        this.$content = this.$body.find("#editable_area");
        this.render();
        this.add_button();
        this.loaded = true;
        // reactivate all the buttons when the field's content (the iframe) is loaded
        $('.o_cp_buttons, .o_statusbar_buttons').find('button').removeClass('o_disabled').attr('disabled', false);
        setTimeout(self.resize, 0);
    },
    on_editor_loaded: function (EditorBar) {
        var self = this;
        this.editor = EditorBar;
        if (this.value && window.odoo[self.callback+"_updown"] && !(this.$content.html()||"").length) {
            this.render();
        }
        setTimeout(function () {
            setTimeout(self.resize,0);
        }, 0);
    },
    add_button: function () {
        var self = this;
        var $to = this.$body.find("#web_editor-top-edit, #wrapwrap").first();

        $(QWeb.render('web_editor.FieldTextHtml.fullscreen'))
            .appendTo($to)
            .on('click', '.o_fullscreen', function () {
                $("body").toggleClass("o_field_widgetTextHtml_fullscreen");
                var full = $("body").hasClass("o_field_widgetTextHtml_fullscreen");
                self.$iframe.parents().toggleClass('o_form_fullscreen_ancestor', full);
                $(window).trigger("resize"); // induce a resize() call and let other backend elements know (the navbar extra items management relies on this)
            });

        this.$body.on('click', '[data-action="cancel"]', function (event) {
            event.preventDefault();
            self.old_initialize_content();
        });
    },
    render: function () {
        var value = (this.value || "").replace(/^<p[^>]*>(\s*|<br\/?>)<\/p>$/, '');
        if (!this.$content) {
            return;
        }
        if (this.mode === "edit") {
            if (window.odoo[this.callback+"_updown"]) {
                // FIXME
                // window.odoo[this.callback+"_updown"](value, this.view.get_fields_values(), this.name);
                this.resize();
            }
        } else {
            this.$content.html(value);
            if (this.$iframe[0].contentWindow) {
                this.$iframe.css("height", (this.$body.height()+20) + "px");
            }
        }
    },
    has_no_value: function () {
        return this.value === false || !this.$content.html() || !this.$content.html().match(/\S/);
    },
    destroy: function () {
        $(window).off('resize', this.resize);
        delete window.odoo[this.callback+"_editor"];
        delete window.odoo[this.callback+"_content"];
        delete window.odoo[this.callback+"_updown"];
        delete window.odoo[this.callback+"_downup"];
    },

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

    /**
     * @override
     */
    commitChanges: function () {
        if (!this.loaded || this.mode === 'readonly') {
            return;
        }
        // switch to WYSIWYG mode if currently in code mode to get all changes
        if (config.debug && this.mode === 'edit' && this.editor.rte) {
            var layoutInfo = this.editor.rte.editable().data('layoutInfo');
            $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
        }
        this.editor.snippetsMenu && this.editor.snippetsMenu.cleanForSave();
        this._setValue(this.$content.html());
        return this._super.apply(this, arguments);
    },
});

field_registry
    .add('html', FieldTextHtmlSimple)
    .add('html_frame', FieldTextHtml);

return {
    FieldTextHtmlSimple: FieldTextHtmlSimple,
    FieldTextHtml: FieldTextHtml,
};
});
Exemplo n.º 2
0
odoo.define('mail.Followers', function (require) {
"use strict";

var AbstractField = require('web.AbstractField');
var concurrency = require('web.concurrency');
var core = require('web.core');
var Dialog = require('web.Dialog');
var field_registry = require('web.field_registry');

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

// -----------------------------------------------------------------------------
// Followers ('mail_followers') widget
// -----------------------------------------------------------------------------
var Followers = AbstractField.extend({
    template: 'mail.Followers',
    events: {
        // click on '(Un)Follow' button, that toggles the follow for uid
        'click .o_followers_follow_button': '_onFollowButtonClicked',
        // click on a subtype, that (un)subscribes for this subtype
        'click .o_subtypes_list .custom-checkbox': '_onSubtypeClicked',
        // click on 'invite' button, that opens the invite wizard
        'click .o_add_follower': '_onAddFollower',
        'click .o_add_follower_channel': '_onAddChannel',
        // click on 'edit_subtype' (pencil) button to edit subscription
        'click .o_edit_subtype': '_onEditSubtype',
        'click .o_remove_follower': '_onRemoveFollower',
        'click .o_mail_redirect': '_onRedirect',
    },
    supportedFieldTypes: ['one2many'],

    // inherited
    init: function (parent, name, record, options) {
        this._super.apply(this, arguments);

        this.image = this.attrs.image || 'image_small';
        this.comment = this.attrs.help || false;

        this.followers = [];
        this.subtypes = [];
        this.data_subtype = {};
        this._isFollower = undefined;
        var session = this.getSession();
        this.partnerID = session.partner_id;

        this.dp = new concurrency.DropPrevious();

        options = options || {};
        this.isEditable = options.isEditable;
    },
    _render: function () {
        // note: the rendering of this widget is asynchronous as it needs to
        // fetch the details of the followers, but it performs a first rendering
        // synchronously (_displayGeneric), and updates its rendering once it
        // has fetched the required data, so this function doesn't return a deferred
        // as we don't want to wait to the data to be loaded to display the widget
        var self = this;
        var fetch_def = this.dp.add(this._readFollowers());

        concurrency.rejectAfter(concurrency.delay(0), fetch_def).then(this._displayGeneric.bind(this));

        fetch_def.then(function () {
            self._displayButtons();
            self._displayFollowers(self.followers);
            if (self.subtypes) { // current user is follower
                self._displaySubtypes(self.subtypes);
            }
        });
    },
    isSet: function () {
        return true;
    },
    _reset: function (record) {
        this._super.apply(this, arguments);
        // the mail widgets being persistent, one need to update the res_id on reset
        this.res_id = record.res_id;
    },

    // public
    getFollowers: function () {
        return this.followers;
    },

    // private
    _displayButtons: function () {
        if (this._isFollower) {
            this.$('button.o_followers_follow_button').removeClass('o_followers_notfollow').addClass('o_followers_following');
            this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false);
            this.$('.o_followers_actions .dropdown-toggle').addClass('o_followers_following');
        } else {
            this.$('button.o_followers_follow_button').removeClass('o_followers_following').addClass('o_followers_notfollow');
            this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true);
            this.$('.o_followers_actions .dropdown-toggle').removeClass('o_followers_following');
        }
        this.$('button.o_followers_follow_button').attr("aria-pressed", this.is_follower);
    },
    _displayGeneric: function () {
        // only display the number of followers (e.g. if read failed)
        this.$('.o_followers_actions').hide();
        this.$('.o_followers_title_box > button').prop('disabled', true);
        this.$('.o_followers_count')
            .html(this.value.res_ids.length)
            .parent().attr('title', this._formatFollowers(this.value.res_ids.length));
    },
    _displayFollowers: function () {
        var self = this;

        // render the dropdown content
        var $followers_list = this.$('.o_followers_list').empty();
        $(QWeb.render('mail.Followers.add_more', {widget: this})).appendTo($followers_list);
        var $follower_li;
        _.each(this.followers, function (record) {
            $follower_li = $(QWeb.render('mail.Followers.partner', {
                'record': _.extend(record, {'avatar_url': '/web/image/' + record.res_model + '/' + record.res_id + '/image_small'}),
                'widget': self})
            );
            $follower_li.appendTo($followers_list);

            // On mouse-enter it will show the edit_subtype pencil.
            if (record.is_editable) {
                $follower_li.on('mouseenter mouseleave', function (e) {
                    $(e.currentTarget).find('.o_edit_subtype').toggleClass('d-none', e.type === 'mouseleave');
                });
            }
        });

        // clean and display title
        this.$('.o_followers_actions').show();
        this.$('.o_followers_title_box > button').prop('disabled', !$followers_list.children().length);
        this.$('.o_followers_count')
            .html(this.value.res_ids.length)
            .parent().attr('title', this._formatFollowers(this.value.res_ids.length));
    },
    _displaySubtypes:function (data, dialog, display_warning) {
        var old_parent_model;
        var $list;
        if (dialog) {
            $list = $('<div>').appendTo(this.dialog.$el);
        } else {
            $list = this.$('.o_subtypes_list .dropdown-menu');
        }
        $list.empty();

        this.data_subtype = data;

        _.each(data, function (record) {
            if (old_parent_model !== record.parent_model && old_parent_model !== undefined) {
                $list.append($('<div>', {class: 'dropdown-divider'}));
            }
            old_parent_model = record.parent_model;
            record.followed = record.followed || undefined;
            $list.append(QWeb.render('mail.Followers.subtype', {
                'record': record,
                'dialog': dialog,
                'display_warning': display_warning && record.internal,
            }));
        });

        if (display_warning) {
            $(QWeb.render('mail.Followers.subtypes.warning')).appendTo(this.dialog.$el);
        }
    },
    _inviteFollower: function (channel_only) {
        var action = {
            type: 'ir.actions.act_window',
            res_model: 'mail.wizard.invite',
            view_mode: 'form',
            view_type: 'form',
            views: [[false, 'form']],
            name: _t("Invite Follower"),
            target: 'new',
            context: {
                'default_res_model': this.model,
                'default_res_id': this.res_id,
                'mail_invite_follower_channel_only': channel_only,
            },
        };
        this.do_action(action, {
            on_close: this._reload.bind(this),
        });
    },
    _formatFollowers: function (count){
        var str = '';
        if (count <= 0) {
            str = _t("No follower");
        } else if (count === 1){
            str = _t("One follower");
        } else {
            str = ''+count+' '+_t("followers");
        }
        return str;
    },
    _readFollowers: function () {
        var self = this;
        var missing_ids = _.difference(this.value.res_ids, _.pluck(this.followers, 'id'));
        var def;
        if (missing_ids.length) {
            def = this._rpc({
                    route: '/mail/read_followers',
                    params: { follower_ids: missing_ids, res_model: this.model }
                });
        }
        return $.when(def).then(function (results) {
            if (results) {
                self.followers = _.uniq(results.followers.concat(self.followers), 'id');
                if (results.subtypes) { //read_followers will return False if current user is not in the list
                    self.subtypes = results.subtypes;
                }
            }
            // filter out previously fetched followers that are no longer following
            self.followers = _.filter(self.followers, function (follower) {
                return _.contains(self.value.res_ids, follower.id);
            });
            var user_follower = _.filter(self.followers, function (rec) { return rec.is_uid; });
            self._isFollower = user_follower.length >= 1;
        });
    },
    _reload: function () {
        this.trigger_up('reload', {fieldNames: [this.name]});
    },
    _follow: function () {
        var kwargs = {
            partner_ids: [this.partnerID],
            context: {}, // FIXME
        };
        this._rpc({
                model: this.model,
                method: 'message_subscribe',
                args: [[this.res_id]],
                kwargs: kwargs,
            })
            .then(this._reload.bind(this));
    },
    /**
     * Remove partners or channels from the followers
     * @param {Array} [ids.partner_ids] the partner ids
     * @param {Array} [ids.channel_ids] the channel ids
     */
    _unfollow: function (ids) {
        var self = this;
        var def = $.Deferred();
        var text = _t("Warning! \n If you remove a follower, he won't be notified of any email or discussion on this document.\n Do you really want to remove this follower ?");
        Dialog.confirm(this, text, {
            confirm_callback: function () {
                var args = [
                    [self.res_id],
                    ids.partner_ids,
                    ids.channel_ids,
                    {}, // FIXME
                ];
                self._rpc({
                        model: self.model,
                        method: 'message_unsubscribe',
                        args: args
                    })
                    .then(self._reload.bind(self));
                def.resolve();
            },
            cancel_callback: def.reject.bind(def),
        });
        return def;
    },
    _updateSubscription: function (event, followerID, isChannel) {
        var ids = {};
        var subtypes;

        if (followerID !== undefined) {
            // Subtypes edited from the modal
            subtypes = this.dialog.$('input[type="checkbox"]');
            if (isChannel) {
                ids.channel_ids = [followerID];
            } else {
                ids.partner_ids = [followerID];
            }
        } else {
            subtypes = this.$('.o_followers_actions input[type="checkbox"]');
            ids.partner_ids = [this.partnerID];
        }

        // Get the subtype ids
        var checklist = [];
        _.each(subtypes, function (record) {
            if ($(record).is(':checked')) {
                checklist.push(parseInt($(record).data('id')));
            }
        });

        // If no more subtype followed, unsubscribe the follower
        if (!checklist.length) {
            this._unfollow(ids).fail(function () {
                $(event.currentTarget).find('input').addBack('input').prop('checked', true);
            });
        } else {
            var kwargs = _.extend({}, ids);
            if (followerID === undefined || followerID === this.partnerID) {
                //this.subtypes will only be updated if the current user
                //just added himself to the followers. We need to update
                //the subtypes manually when editing subtypes
                //for current user
                _.each(this.subtypes, function (subtype) {
                    subtype.followed = checklist.indexOf(subtype.id) > -1;
                });
            }
            kwargs.subtype_ids = checklist;
            kwargs.context = {}; // FIXME
            this._rpc({
                    model: this.model,
                    method: 'message_subscribe',
                    args: [[this.res_id]],
                    kwargs: kwargs,
                })
                .then(this._reload.bind(this));
        }
    },

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

    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onAddFollower: function (ev) {
        ev.preventDefault();
        this._inviteFollower(false);
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onAddChannel: function (ev) {
        ev.preventDefault();
        this._inviteFollower(true);
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onEditSubtype: function (ev) {
        var self = this;
        var $currentTarget = $(ev.currentTarget);
        var follower_id = $currentTarget.data('follower-id'); // id of model mail_follower
        this._rpc({
                route: '/mail/read_subscription_data',
                params: {res_model: this.model, follower_id: follower_id},
            })
            .then(function (data) {
                var res_id = $currentTarget.data('oe-id'); // id of model res_partner or mail_channel
                var is_channel = $currentTarget.data('oe-model') === 'mail.channel';
                self.dialog = new Dialog(this, {
                    size: 'medium',
                    title: _t("Edit Subscription of ") + $currentTarget.siblings('a').text(),
                    buttons: [
                        {
                            text: _t("Apply"),
                            classes: 'btn-primary',
                            click: function () {
                                self._updateSubscription(ev, res_id, is_channel);
                            },
                            close: true
                        },
                        {
                            text: _t("Cancel"),
                            close: true,
                        },
                    ],
                });
                self.dialog.opened().then(function () {
                    self._displaySubtypes(data, true, is_channel);
                });
                self.dialog.open();
            });
    },
    /**
     * @private
     */
    _onFollowButtonClicked: function () {
        if (!this._isFollower) {
            this._follow();
        } else {
            this._unfollow({partner_ids: [this.partnerID]});
        }
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onRedirect: function (ev) {
        ev.preventDefault();
        var $target = $(ev.target);
        this.do_action({
            type: 'ir.actions.act_window',
            view_type: 'form',
            view_mode: 'form',
            res_model: $target.data('oe-model'),
            views: [[false, 'form']],
            res_id: $target.data('oe-id'),
        });
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onRemoveFollower: function (ev) {
        var resModel = $(ev.target).parent().find('a').data('oe-model');
        var resID = $(ev.target).parent().find('a').data('oe-id');
        if (resModel === 'res.partner') {
            return this._unfollow({partner_ids: [resID]});
        } else {
            return this._unfollow({channel_ids: [resID]});
        }
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onSubtypeClicked: function (ev) {
        ev.stopPropagation();
        this._updateSubscription(ev);
        var $list = this.$('.o_subtypes_list');
        if (!$list.hasClass('show')) {
            $list.addClass('show');
        }
        if (this.$('.o_subtypes_list .dropdown-menu')[0].children.length < 1) {
            $list.removeClass('show');
        }
    },
});

field_registry.add('mail_followers', Followers);

return Followers;

});
Exemplo n.º 3
0
Arquivo: lunch.js Projeto: 10537/odoo
odoo.define('lunch.previous_orders', function (require) {
"use strict";

var AbstractField = require('web.AbstractField');
var core = require('web.core');
var field_registry = require('web.field_registry');
var field_utils = require('web.field_utils');

var QWeb = core.qweb;

var LunchPreviousOrdersWidget = AbstractField.extend({
    events: {
        'click .o_add_button': '_onAddOrder',
    },
    supportedFieldTypes: ['one2many'],
    /**
     * @override
     */
    init: function () {
        this._super.apply(this, arguments);
        this.lunchData = JSON.parse(this.value);
    },

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

    /**
     * Used by the widget to render the previous order price like a monetary.
     *
     * @private
     * @param {Object} order
     * @returns {string} the monetary formatting of order price
     */
    _formatValue: function (order) {
        var options = _.extend({}, this.nodeOptions, order);
        return field_utils.format.monetary(order.price, this.field, options);
    },
    /**
     * @private
     * @override
     */
    _render: function () {
        if (this.lunchData !== false) {
            // group data by supplier for display
            var categories = _.groupBy(this.lunchData, 'supplier');
            this.$el.html(QWeb.render('LunchPreviousOrdersWidgetList', {
                formatValue: this._formatValue.bind(this),
                categories: categories,
            }));
        } else {
            return this.$el.html(QWeb.render('LunchPreviousOrdersWidgetNoOrder'));
        }
    },

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

    /**
     * @private
     * @param {MouseEvent} event
     */
    _onAddOrder: function (event) {
        // Get order details from line
        var lineID = parseInt($(event.currentTarget).data('id'));
        if (!lineID) {
            return;
        }
        var values = {
            product_id: {
                id: this.lunchData[lineID].product_id,
                display_name: this.lunchData[lineID].product_name,
            },
            note: this.lunchData[lineID].note,
            price: this.lunchData[lineID].price,
        };

        // create a new order line
        this.trigger_up('field_changed', {
            dataPointID: this.dataPointID,
            changes: {
                order_line_ids: {
                    operation: 'CREATE',
                    data: values,
                },
            },
        });
    },
});

field_registry.add('previous_order', LunchPreviousOrdersWidget);

});
Exemplo n.º 4
0
QUnit.test('specification of widget barcode_handler with keypress and notifyChange', function (assert) {
    assert.expect(6);
    var done = assert.async();

    var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms;
    barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0;

    this.data.order.onchanges = {
        _barcode_scanned: function () {},
    };

    // Define a specific barcode_handler widget for this test case
    var TestBarcodeHandler = AbstractField.extend({
        init: function () {
            this._super.apply(this, arguments);

            this.trigger_up('activeBarcode', {
                name: 'test',
                fieldName: 'line_ids',
                notifyChange: false,
                setQuantityWithKeypress: true,
                quantity: 'quantity',
                commands: {
                    barcode: '_barcodeAddX2MQuantity',
                }
            });
        },
    });
    fieldRegistry.add('test_barcode_handler', TestBarcodeHandler);

    var form = createView({
        View: FormView,
        model: 'order',
        data: this.data,
        arch: '<form>' +
                    '<field name="_barcode_scanned" widget="test_barcode_handler"/>' +
                    '<field name="line_ids">' +
                        '<tree>' +
                            '<field name="product_id"/>' +
                            '<field name="product_barcode" invisible="1"/>' +
                            '<field name="quantity"/>' +
                        '</tree>' +
                    '</field>' +
                '</form>',
        mockRPC: function (route, args) {
            assert.step(args.method);
            return this._super.apply(this, arguments);
        },
        res_id: 1,
        viewOptions: {
            mode: 'edit',
        },
    });
    _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent);
         // Quantity listener should open a dialog.
    triggerKeypressEvent('5');

    setTimeout(function () {
        var keycode = $.ui.keyCode.ENTER;

        assert.strictEqual($('.modal .modal-body').length, 1, 'should open a modal with a quantity as input');
        assert.strictEqual($('.modal .modal-body .o_set_qty_input').val(), '5', 'the quantity by default in the modal shoud be 5');

        $('.modal .modal-body .o_set_qty_input').val('7');

        $('.modal .modal-body .o_set_qty_input').trigger($.Event('keypress', {which: keycode, keyCode: keycode}));
        assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(1)').text(), '7',
        "quantity checked should be 7");

        assert.verifySteps(['read', 'read']);

        form.destroy();
        barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay;
        delete fieldRegistry.map.test_barcode_handler;
        done();
    });
});
Exemplo n.º 5
0
QUnit.test('specification of widget barcode_handler', function (assert) {
    assert.expect(5);

    var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms;
    barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0;

    // Define a specific barcode_handler widget for this test case
    var TestBarcodeHandler = AbstractField.extend({
        init: function () {
            this._super.apply(this, arguments);

            this.trigger_up('activeBarcode', {
                name: 'test',
                fieldName: 'line_ids',
                quantity: 'quantity',
                commands: {
                    barcode: '_barcodeAddX2MQuantity',
                }
            });
        },
    });
    fieldRegistry.add('test_barcode_handler', TestBarcodeHandler);

    var form = createView({
        View: FormView,
        model: 'order',
        data: this.data,
        arch: '<form>' +
                    '<field name="_barcode_scanned" widget="test_barcode_handler"/>' +
                    '<field name="line_ids">' +
                        '<tree>' +
                            '<field name="product_id"/>' +
                            '<field name="product_barcode" invisible="1"/>' +
                            '<field name="quantity"/>' +
                        '</tree>' +
                    '</field>' +
                '</form>',
        mockRPC: function (route, args) {
            if (args.method === 'onchange') {
                assert.notOK(true, "should not do any onchange RPC");
            }
            if (args.method === 'write') {
                assert.deepEqual(args.args[1].line_ids, [
                    [1, 1, {quantity: 2}], [1, 2, {quantity: 1}],
                ], "should have generated the correct commands");
            }
            return this._super.apply(this, arguments);
        },
        res_id: 1,
        viewOptions: {
            mode: 'edit',
        },
    });

    assert.strictEqual(form.$('.o_data_row').length, 2,
        "one2many should contain 2 rows");

    // scan twice product 1
    _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent);
    assert.strictEqual(form.$('.o_data_row:first .o_data_cell:nth(1)').text(), '1',
        "quantity of line one should have been incremented");
    _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent);
    assert.strictEqual(form.$('.o_data_row:first .o_data_cell:nth(1)').text(), '2',
        "quantity of line one should have been incremented");

    // scan once product 2
    _.each(['0','9','8','7','6','5','4','3','2','1','Enter'], triggerKeypressEvent);
    assert.strictEqual(form.$('.o_data_row:nth(1) .o_data_cell:nth(1)').text(), '1',
        "quantity of line one should have been incremented");

    form.$buttons.find('.o_form_button_save').click();

    form.destroy();
    barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay;
    delete fieldRegistry.map.test_barcode_handler;
});
Exemplo n.º 6
0
odoo.define('pad.pad', function (require) {
"use strict";

var AbstractField = require('web.AbstractField');
var core = require('web.core');
var fieldRegistry = require('web.field_registry');

var _t = core._t;

var FieldPad = AbstractField.extend({
    template: 'FieldPad',
    content: "",
    events: {
        'click .oe_pad_switch': '_onToggleFullScreen',
    },

    /**
     * @override
     */
    willStart: function () {
        if (this.isPadConfigured === undefined) {
            return this._rpc({
                method: 'pad_is_configured',
                model: this.model,
            }).then(function (result) {
                // we write on the prototype to share the information between
                // all pad widgets instances, across all actions
                FieldPad.prototype.isPadConfigured = result;
            });
        }
        return this._super.apply(this, arguments);
    },
    /**
     * @override
     */
    start: function () {
        if (!this.isPadConfigured) {
            this.$(".oe_unconfigured").removeClass('d-none');
            this.$(".oe_configured").addClass('d-none');
            return Promise.resolve();
        }
        var defs = [];
        if (this.mode === 'edit' && _.str.startsWith(this.value, 'http')) {
            this.url = this.value;
            // please close your eyes and look elsewhere...
            // Since the pad value (the url) will not change during the edition
            // process, we have a problem: the description field will not be
            // properly updated.  We need to explicitely write the value each
            // time someone edit the record in order to force the server to read
            // the updated value of the pad and put it in the description field.
            //
            // However, the basic model optimizes away the changes if they are
            // not really different from the current value. So, we need to
            // either add special configuration options to the basic model, or
            // to trick him into accepting the same value as being different...
            // Guess what we decided...
            var url = {};
            url.toJSON = _.constant(this.url);
            defs.push(this._setValue(url, {doNotSetDirty: true}));
        }

        defs.push(this._super.apply(this, arguments));
        return Promise.all(defs);
    },

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

    /**
     * If we had to generate an url, we wait for the generation to be completed,
     * so the current record will be associated with the correct pad url.
     *
     * @override
     */
    commitChanges: function () {
        return this.urlDef;
    },
    /**
     * @override
     */
    isSet: function () {
        return true;
    },

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

    /**
     * Note that this method has some serious side effects: performing rpcs and
     * setting the value of this field.  This is not conventional and should not
     * be copied in other code, unless really necessary.
     *
     * @override
     * @private
     */
    _renderEdit: function () {
        if (this.url) {
            // here, we have a valid url, so we can simply display an iframe
            // with the correct src attribute
            var userName = encodeURIComponent(this.getSession().userName);
            var url = this.url + '?showChat=false&userName='******'<iframe width="100%" height="100%" frameborder="0" src="' + url + '"></iframe>';
            this.$('.oe_pad_content').html(content);
        } else if (this.value) {
            // it looks like the field does not contain a valid url, so we just
            // display it (it cannot be edited in that case)
            this.$('.oe_pad_content').text(this.value);
        } else {
            // It is totally discouraged to have a render method that does
            // non-rendering work, especially since the work in question
            // involves doing RPCs and changing the value of the field.
            // However, this is kind of necessary in this case, because the
            // value of the field is actually only the url of the pad. The
            // actual content will be loaded in an iframe.  We could do this
            // work in the basic model, but the basic model does not know that
            // this widget is in edit or readonly, and we really do not want to
            // create a pad url everytime a task without a pad is viewed.
            var self = this;
            this.urlDef = this._rpc({
                method: 'pad_generate_url',
                model: this.model,
                context: {
                    model: this.model,
                    field_name: this.name,
                    object_id: this.res_id
                },
            }, {
                shadow: true
            }).then(function (result) {
                // We need to write the url of the pad to trigger
                // the write function which updates the actual value
                // of the field to the value of the pad content
                self.url = result.url;
                self._setValue(result.url, {doNotSetDirty: true});
            });
        }
    },
    /**
     * @override
     * @private
     */
    _renderReadonly: function () {
        if (_.str.startsWith(this.value, 'http')) {
            var self = this;
            this.$('.oe_pad_content')
                .addClass('oe_pad_loading')
                .text(_t("Loading"));
            this._rpc({
                method: 'pad_get_content',
                model: this.model,
                args: [this.value]
            }, {
                shadow: true
            }).then(function (data) {
                self.$('.oe_pad_content')
                    .removeClass('oe_pad_loading')
                    .html('<div class="oe_pad_readonly"><div>');
                self.$('.oe_pad_readonly').html(data);
            }).guardedCatch(function () {
                self.$('.oe_pad_content').text(_t('Unable to load pad'));
            });
        } else {
            this.$('.oe_pad_content')
                .addClass('oe_pad_loading')
                .show()
                .text(_t("This pad will be initialized on first edit"));
        }
    },

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

    /**
     * @override
     * @private
     */
    _onToggleFullScreen: function () {
        this.$el.toggleClass('oe_pad_fullscreen mb0');
        this.$('.oe_pad_switch').toggleClass('fa-expand fa-compress');
        this.$el.parents('.o_touch_device').toggleClass('o_scroll_hidden');
    },
});

fieldRegistry.add('pad', FieldPad);

return FieldPad;

});
Exemplo n.º 7
0
odoo.define('mail.ThreadField', function (require) {
"use strict";

var ChatThread = require('mail.ChatThread');

var AbstractField = require('web.AbstractField');
var core = require('web.core');
var field_registry = require('web.field_registry');
var concurrency = require('web.concurrency');
var session = require('web.session');

var _t = core._t;

// -----------------------------------------------------------------------------
// 'mail_thread' widget: displays the thread of messages
// -----------------------------------------------------------------------------
var ThreadField = AbstractField.extend({
    // inherited
    init: function () {
        this._super.apply(this, arguments);
        this.msgIDs = this.value.res_ids;
    },
    willStart: function () {
        return this.alive(this.call('chat_manager', 'isReady'));
    },
    start: function () {
        var self = this;

        this.dp = new concurrency.DropPrevious();

        this.thread = new ChatThread(this, {
            display_order: ChatThread.ORDER.DESC,
            display_document_link: false,
            display_needactions: false,
            squash_close_messages: false,
        });

        this.thread.on('load_more_messages', this, this._onLoadMoreMessages);
        this.thread.on('redirect', this, this._onRedirect);
        this.thread.on('redirect_to_channel', this, this._onRedirectToChannel);
        this.thread.on('toggle_star_status', this, function (messageID) {
            self.call('chat_manager', 'toggleStarStatus', messageID);
        });

        var def1 = this.thread.appendTo(this.$el);
        var def2 = this._super.apply(this, arguments);

        return this.alive($.when(def1, def2)).then(function () {
            // unwrap the thread to remove an unnecessary level on div
            self.setElement(self.thread.$el);
            var chatBus = self.call('chat_manager', 'getChatBus');
            chatBus.on('new_message', self, self._onNewMessage);
            chatBus.on('update_message', self, self._onUpdateMessage);
        });
    },
    _render: function () {
        return this._fetchAndRenderThread(this.msgIDs);
    },
    isSet: function () {
        return true;
    },
    destroy: function () {
        this.call('chat_manager', 'removeChatterMessages', this.model);
        this._super.apply(this, arguments);
    },
    _reset: function (record) {
        this._super.apply(this, arguments);
        this.msgIDs = this.value.res_ids;
        // the mail widgets being persistent, one need to update the res_id on reset
        this.res_id = record.res_id;
    },

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

    /**
     * @param  {Object} message
     * @param  {integer[]} message.partner_ids
     * @return {$.Promise}
     */
    postMessage: function (message) {
        var self = this;
        var options = {model: this.model, res_id: this.res_id};
        return this.call('chat_manager', 'postMessage', message, options)
            .then(function () {
                if (message.partner_ids.length) {
                    self.trigger_up('reload_mail_fields', {followers: true});
                }
            })
            .fail(function () {
                self.do_notify(_t('Sending Error'), _t('Your message has not been sent.'));
            });
    },

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

    /**
     * @private
     * @returns {Object[]} an array containing a single message 'Creating a new record...'
     */
    _forgeCreationModeMessages: function () {
        return [{
            id: 0,
            body: "<p>Creating a new record...</p>",
            date: moment(),
            author_id: [session.partner_id, session.partner_display_name],
            displayed_author: session.partner_display_name,
            avatar_src: "/web/image/res.partner/" + session.partner_id + "/image_small",
            attachment_ids: [],
            customer_email_data: [],
            tracking_value_ids: [],
        }];
    },
    /**
     * @private
     * @param {integer[]} ids
     * @param {Object} [options]
     */
    _fetchAndRenderThread: function (ids, options) {
        var self = this;
        options = options || {};
        options.ids = ids;
        var fetch_def = this.dp.add(this.call('chat_manager', 'getMessages', options));
        return fetch_def.then(function (raw_messages) {
            var isCreateMode = false;
            if (!self.res_id) {
                raw_messages = self._forgeCreationModeMessages();
                isCreateMode = true;
            }
            self.thread.render(raw_messages, {
                display_load_more: raw_messages.length < ids.length,
                isCreateMode: isCreateMode,
            });
        });
    },

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

    /**
     * When a new message arrives, fetch its data to render it
     * @param {Number} message_id : the identifier of the new message
     * @returns {Deferred}
     */
    _onLoadMoreMessages: function () {
        this._fetchAndRenderThread(this.msgIDs, {forceFetch: true});
    },
    _onNewMessage: function (message) {
        if (message.model === this.model && message.res_id === this.res_id) {
            this.msgIDs.unshift(message.id);
            this.trigger_up('new_message', {
                id: this.value.id,
                msgIDs: this.msgIDs,
            });
            this._fetchAndRenderThread(this.msgIDs);
        }
    },
    _onRedirectToChannel: function (channelID) {
        var self = this;
        this.call('chat_manager', 'joinChannel', channelID).then(function () {
            // Execute Discuss client action with 'channel' as default channel
            self.do_action('mail.mail_channel_action_client_chat', {active_id: channelID});
        });
    },
    _onRedirect: function (res_model, res_id) {
        this.trigger_up('redirect', {
            res_id: res_id,
            res_model: res_model,
        });
    },
    _onUpdateMessage: function (message) {
        if (message.model === this.model && message.res_id === this.res_id) {
            this._fetchAndRenderThread(this.msgIDs);
        }
    },
});

field_registry.add('mail_thread', ThreadField);

return ThreadField;

});