diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index dc06344f35c..5f202f95982 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -45,6 +45,7 @@ 'application': True, 'qweb': [ 'static/src/xml/activity.xml', + 'static/src/xml/activity_view.xml', 'static/src/xml/composer.xml', 'static/src/xml/chatter.xml', 'static/src/xml/discuss.xml', diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index fda0b5f88de..a1eb827db98 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -147,6 +147,42 @@ class MailActivity(models.Model): help='Technical field for UX purpose') mail_template_ids = fields.Many2many(related='activity_type_id.mail_template_ids') + @api.model + def get_activity_data(self, res_model, domain): + res = self.env[res_model].search(domain) + activity_domain = [('res_id', 'in', res.ids), ('res_model', '=', res_model)] + grouped_activities = self.env['mail.activity'].read_group(activity_domain, ['res_id', 'activity_type_id', 'res_name:max(res_name)', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)'], ['res_id', 'activity_type_id'], lazy=False) + activity_type_ids = self.env['mail.activity.type'] + res_list = set() + activity_data = defaultdict(dict) + for group in grouped_activities: + res_id = group['res_id'] + res_name = group['res_name'] + activity_type_id = group['activity_type_id'][0] + activity_type_ids |= self.env['mail.activity.type'].browse(activity_type_id) # we will get the name when reading mail_template_ids + res_list.add((res_id, res_name)) + state = self._compute_state_from_date(group['date_deadline'], self.user_id.sudo().tz) + activity_data[res_id][activity_type_id] = { + 'count': group['__count'], + 'domain': group['__domain'], + 'ids': group['ids'], + 'state': state, + 'o_closest_deadline': group['date_deadline'], + } + activity_type_infos = [] + for elem in activity_type_ids: + mail_template_info = [] + for mail_template_id in elem.mail_template_ids: + mail_template_info.append({"id": mail_template_id.id, "name": mail_template_id.name}) + activity_type_infos.append([elem.id, elem.name, mail_template_info]) + + return { + 'activity_types': activity_type_infos, + 'res_ids': list(res_list), + 'grouped_activities': activity_data, + 'model': res_model, + } + @api.multi @api.onchange('previous_activity_type_id') def _compute_has_recommended_activities(self): @@ -167,24 +203,27 @@ class MailActivity(models.Model): @api.depends('date_deadline') def _compute_state(self): - today_default = date.today() - for record in self.filtered(lambda activity: activity.date_deadline): - today = today_default tz = record.user_id.sudo().tz - if tz: - today_utc = pytz.UTC.localize(datetime.utcnow()) - today_tz = today_utc.astimezone(pytz.timezone(tz)) - today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day) + date_deadline = record.date_deadline + record.state = self._compute_state_from_date(date_deadline, tz) - date_deadline = fields.Date.from_string(record.date_deadline) - diff = (date_deadline - today) - if diff.days == 0: - record.state = 'today' - elif diff.days < 0: - record.state = 'overdue' - else: - record.state = 'planned' + @api.model + def _compute_state_from_date(self, date_deadline, tz=False): + date_deadline = fields.Date.from_string(date_deadline) + today_default = date.today() + today = today_default + if tz: + today_utc = pytz.UTC.localize(datetime.utcnow()) + today_tz = today_utc.astimezone(pytz.timezone(tz)) + today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day) + diff = (date_deadline - today) + if diff.days == 0: + return 'today' + elif diff.days < 0: + return 'overdue' + else: + return 'planned' @api.onchange('activity_type_id') def _onchange_activity_type_id(self): diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index dbff89c76db..0a25fcfe194 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -146,6 +146,7 @@ var AbstractActivityField = AbstractField.extend({ context: this.record.getContext(), }).then( function(rslt_action) { + console.log(rslt_action); self.do_action(rslt_action, { on_close: function () { self.trigger_up('reload'); diff --git a/addons/mail/static/src/js/views/activity/activity_controller.js b/addons/mail/static/src/js/views/activity/activity_controller.js new file mode 100644 index 00000000000..6af4c310c01 --- /dev/null +++ b/addons/mail/static/src/js/views/activity/activity_controller.js @@ -0,0 +1,74 @@ +odoo.define('mail.ActivityController', function (require) { +"use strict"; + +var AbstractController = require('web.AbstractController'); + +var ActivityController = AbstractController.extend({ + custom_events: _.extend({}, AbstractController.prototype.custom_events, { + send_mail_template: '_onSendMailTemplate', + open_view_form: '_onOpenViewForm', + reload: '_onReload', + }), + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} event + * @param {string} event.name + * @param {Object} event.data + * @param {boolean} [event.data.activity] + * @param {boolean} [event.data.followers] + * @param {boolean} [event.data.thread] + */ + _onReload: function (event) { + event.stopPropagation(); + var self = this; + this.model.reload().then(self.reload()); + }, + + /** + * @private + * @param {OdooEvent} ev + */ + _onSendMailTemplate: function (ev) { + var templateID = ev.data.templateID; + var activityTypeID = ev.data.activityTypeID; + var state = this.model.get(); + var groupedActivities = state.data.grouped_activities; + var resIDS = []; + Object.keys(groupedActivities).forEach(function (resID) { + var activityByType = groupedActivities[resID]; + var activity = activityByType[activityTypeID]; + if (activity) { + resIDS.push(parseInt(resID)); + } + }); + this._rpc({ + model: this.model.modelName, + method: 'activity_send_mail', + args: [resIDS, templateID], + }); + }, + /** + * @private + * @override + * @param {MouseEvent} ev + */ + _onOpenViewForm: function (ev) { + var resID = ev.data.resID; + this.do_action({ + type: 'ir.actions.act_window', + res_model: this.model.modelName, + res_id: resID, + views: [[false, 'form']], + target: 'current' + }); + }, +}); + +return ActivityController; + +}); diff --git a/addons/mail/static/src/js/views/activity/activity_model.js b/addons/mail/static/src/js/views/activity/activity_model.js new file mode 100644 index 00000000000..146609bbb52 --- /dev/null +++ b/addons/mail/static/src/js/views/activity/activity_model.js @@ -0,0 +1,69 @@ +odoo.define('mail.ActivityModel', function (require) { +'use strict'; + +var AbstractModel = require('web.AbstractModel'); + +var ActivityModel = AbstractModel.extend({ + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + get: function () { + return this.data; + }, + /** + * @override + * @param {Object} params + * @param {Array[]} params.domain + * @returns {Deferred} + */ + load: function (params) { + this.domain = params.domain; + this.modelName = params.modelName; + this.data = {}; + return this._fetchData(); + }, + /** + * @param {any} handle + * @param {Object} params + * @param {Array[]} params.domain + * @returns {Deferred} + */ + reload: function (handle, params) { + if (params && 'domain' in params) { + this.domain = params.domain; + } + return this._fetchData(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Fetch activity data. + * + * @private + * @returns {Deferred} + */ + _fetchData: function () { + var self = this; + return this._rpc({ + model: "mail.activity", + method: 'get_activity_data', + kwargs: { + res_model: this.modelName, + domain: this.domain, + } + }).then(function (result) { + self.data.data = result; + }); + }, +}); + +return ActivityModel; + +}); diff --git a/addons/mail/static/src/js/views/activity/activity_renderer.js b/addons/mail/static/src/js/views/activity/activity_renderer.js new file mode 100644 index 00000000000..bba1cf86540 --- /dev/null +++ b/addons/mail/static/src/js/views/activity/activity_renderer.js @@ -0,0 +1,181 @@ +odoo.define('mail.ActivityRenderer', function (require) { +"use strict"; + +var AbstractRenderer = require('web.AbstractRenderer'); +var core = require('web.core'); +var field_registry = require('web.field_registry'); + +var QWeb = core.qweb; +var _t = core._t; + +var ActivityRenderer = AbstractRenderer.extend({ + className: 'o_activity_view', + events: { + 'click .o_res_name_cell': '_onResNameClicked', + 'click .o_send_mail_template': '_onSenMailTemplateClicked', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _render: function () { + this.$el + .removeClass('table-responsive') + .empty(); + + if (this.state.data.activity_types.length === 0) { + this.$el.append($("
')) //empty cell for name
+ .append(_.map(this.state.data.activity_types, this._renderHeaderCell.bind(this)));
+ return $('').append($tr);
+ },
+ /**
+ * @private
+ * @param {Object} activity_type
+ * @returns {jQueryElement} a | element
+ */
+ _renderHeaderCell: function (activity_type) {
+ return QWeb.render('mail.ActivityViewHeaderCell', {
+ id: activity_type[0],
+ name: activity_type[1],
+ template_list: activity_type[2] || [],
+ });
+ },
+ /**
+ * @private
+ * @param {Object} data
+ * @returns {jQueryElement} a | ')
+ .addClass("o_res_name_cell")
+ .html(name)
+ .data('res-id', res_id);
+ var $cells = _.map(this.state.data.activity_types, function (node) {
+ var $td = $(' | ').addClass("o_activity_summary_cell");
+ var activity_type_id = node[0];
+ var activity_group = self.state.data.grouped_activities[res_id][activity_type_id];
+ activity_group = activity_group || {count: 0, ids: [], state: false};
+ if (activity_group.state) {
+ $td.addClass(activity_group.state);
+ }
+ // we need to create a fake record in order to instanciate the KanbanActivity
+ // this is the minimal information in order to make it work
+ // AAB: move this to a function
+ var record = {
+ data: {
+ activity_ids: {
+ model: 'mail.activity',
+ res_ids: activity_group.ids,
+ },
+ activity_state: activity_group.state,
+ },
+ fields: {
+ activity_ids: {},
+ activity_state: {
+ selection: [
+ ['overdue', "Overdue"],
+ ['today', "Today"],
+ ['planned', "Planned"],
+ ],
+ },
+ },
+ fieldsInfo: {},
+ model: self.state.data.model,
+ ref: res_id, // not necessary, i think
+ type: 'record',
+ res_id: res_id,
+ getContext: function () {
+ return {}; // session.user_context
+ },
+ //todo intercept event or changes on record to update view
+ };
+ var KanbanActivity = field_registry.get('kanban_activity');
+ var widget = new KanbanActivity(self, "activity_ids", record, {});
+ widget.appendTo($td);
+ // replace clock by closest deadline
+ var $date = $(' | ');
+ var formated_date = moment(activity_group.o_closest_deadline).format('ll');
+ var current_year = (new Date()).getFullYear();
+ if (formated_date.endsWith(current_year)) { // Dummy logic to remove year (only if current year), we will maybe need to improve it
+ formated_date = formated_date.slice(0, -4);
+ formated_date = formated_date.replace(/( |,)*$/g, "");
+ }
+ $date
+ .text(formated_date)
+ .addClass('o_closest_deadline');
+ $td.find('a')
+ .empty()
+ .append($date);
+ return $td;
+ });
+ var $tr = $('
+ |
+ |
|---|