diff --git a/addons/contacts/views/contact_views.xml b/addons/contacts/views/contact_views.xml index ba0fa9b74e0..82da7aaca6b 100644 --- a/addons/contacts/views/contact_views.xml +++ b/addons/contacts/views/contact_views.xml @@ -5,7 +5,7 @@ ir.actions.act_window res.partner form - kanban,tree,form + kanban,tree,form,activity

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($("

").text(_t("No schedulded activity"))); + } else { + var $table = $('') + .addClass('table-bordered') + .append(this._renderHeader()) + .append(this._renderBody()); + this.$el + .addClass('table-responsive') + .append($table); + } + return this._super(); + }, + /** + * @private + * @returns {jQueryElement} a jquery element + */ + _renderBody: function () { + var $rows = _.map(this.state.data.res_ids, this._renderRow.bind(this)); + return $('').append($rows); + }, + /** + * @private + * @returns {jQueryElement} a jquery element + */ + _renderHeader: function () { + var $tr = $('') + .append($('').append($tr); + }, + /** + * @private + * @param {Object} activity_type + * @returns {jQueryElement} a element + */ + _renderRow: function (data) { + var self = this; + var res_id = data[0]; + var name = data[1]; + var $nameTD = $('', {class: 'o_data_row'}) + .append($nameTD) + .append($cells); + return $tr; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + /** + * @private + * @override + * @param {MouseEvent} ev + */ + _onSenMailTemplateClicked: function (ev) { + var $target = $(ev.currentTarget); + var templateID = $target.data('template-id'); + var activityTypeID = $target.closest('th').data('activity-type-id'); + this.trigger_up('send_mail_template', { + activityTypeID: activityTypeID, + templateID: templateID, + }); + }, + /** + * @private + * @override + * @param {MouseEvent} ev + */ + _onResNameClicked: function (ev) { + var resID = $(ev.currentTarget).data('res-id'); + this.trigger_up('open_view_form', {resID: resID}); + }, +}); + +return ActivityRenderer; + +}); diff --git a/addons/mail/static/src/js/views/activity/activity_view.js b/addons/mail/static/src/js/views/activity/activity_view.js new file mode 100644 index 00000000000..fdd258caa54 --- /dev/null +++ b/addons/mail/static/src/js/views/activity/activity_view.js @@ -0,0 +1,29 @@ +odoo.define('mail.ActivityView', function (require) { +"use strict"; + +var ActivityController = require('mail.ActivityController'); +var ActivityModel = require('mail.ActivityModel'); +var ActivityRenderer = require('mail.ActivityRenderer'); +var AbstractView = require('web.AbstractView'); +var core = require('web.core'); +var view_registry = require('web.view_registry'); + +var _lt = core._lt; + +var ActivityView = AbstractView.extend({ + accesskey: "a", + display_name: _lt('Activity'), + icon: 'fa-clock-o', + config: _.extend({}, AbstractView.prototype.config, { + Controller: ActivityController, + Model: ActivityModel, + Renderer: ActivityRenderer, + }), + viewType: 'activity', +}); + +view_registry.add('activity', ActivityView); + +return ActivityView; + +}); diff --git a/addons/mail/static/src/scss/activity_view.scss b/addons/mail/static/src/scss/activity_view.scss new file mode 100644 index 00000000000..d67c6019cdf --- /dev/null +++ b/addons/mail/static/src/scss/activity_view.scss @@ -0,0 +1,54 @@ +.o_activity_view { + height: 100%; + > table { + background-color: white; + } + .o_activity_summary_cell { + background-color: #FFF; + &.planned { + background-color: theme-color('success'); + } + &.overdue { + background-color: theme-color('danger'); + } + &.today { + background-color: theme-color('warning'); + } + .o_kanban_inline_block { + min-height: 42px; + } + .dropdown-toggle { + cursor: pointer; + .o_closest_deadline { + height: 42px; + width: 100%; + color: #FFF; + text-align: center; + line-height: 42px; + } + } + } + .o_res_name_cell { + padding: 0 10px; + color: #008784; + &:hover { + color: #666; + } + } + .o_activity_type_cell { + padding:10px; + min-width:100px; + .fa-ellipsis-v { + cursor: pointer; + } + + .o_template_element { + white-space: nowrap; + padding:5px; + cursor: pointer; + &:hover { + color: theme-color('success'); + } + } + } +} diff --git a/addons/mail/static/src/scss/mail_activity.scss b/addons/mail/static/src/scss/mail_activity.scss index 7fbe0e89c82..0582b5312f4 100644 --- a/addons/mail/static/src/scss/mail_activity.scss +++ b/addons/mail/static/src/scss/mail_activity.scss @@ -144,10 +144,13 @@ } /* Kanban View */ -.o_kanban_record { +.o_kanban_record{ .o_kanban_inline_block { display: inline-block; } +} + +.o_kanban_record, .o_view_controller{ .o_mail_activity { .o_activity_btn { span.fa { diff --git a/addons/mail/static/src/xml/activity_view.xml b/addons/mail/static/src/xml/activity_view.xml new file mode 100644 index 00000000000..eacb4cc7ca9 --- /dev/null +++ b/addons/mail/static/src/xml/activity_view.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 047a49b38c3..a46d02132c4 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -447,7 +447,7 @@ QUnit.test('kanban activity widget popover test', function (assert) { '' + '', mockRPC: function (route, args) { - if (route === '/web/dataset/call_kw/mail.activity/action_feedback') { + if (route === '/web/dataset/call_kw/mail.activity/action_done_schedule_next') { rpcCount++; var current_ids = this.data.partner.records[0].activity_ids; @@ -1195,7 +1195,7 @@ QUnit.test('form activity widget with another x2many field in view', function (a }); QUnit.test('form activity widget: schedule next activity', function (assert) { - assert.expect(5); + assert.expect(4); this.data.partner.records[0].activity_ids = [1]; this.data.partner.records[0].activity_state = 'today'; this.data['mail.activity'].records = [{ @@ -1208,7 +1208,6 @@ QUnit.test('form activity widget: schedule next activity', function (assert) { activity_type_id: 2, }]; - var checkReadArgs = false; var form = createView({ View: FormView, model: 'partner', @@ -1225,35 +1224,17 @@ QUnit.test('form activity widget: schedule next activity', function (assert) { '', res_id: 2, mockRPC: function (route, args) { - if (route === '/web/dataset/call_kw/mail.activity/action_feedback') { - assert.ok(_.isEqual(args.args[0], [1]), "should call 'action_feedback' for id 1"); + if (route === '/web/dataset/call_kw/mail.activity/action_done_schedule_next') { + assert.ok(_.isEqual(args.args[0], [1]), "should call 'action_done_schedule_next' for id 1"); assert.strictEqual(args.kwargs.feedback, 'everything is ok', "the feedback should be sent correctly"); - return $.when(); - } - if (args.method === 'read' && args.model === 'partner' && checkReadArgs) { - assert.deepEqual(args.args[1], ['activity_ids', 'message_ids', 'display_name'], - "should only read the mail fields"); + return $.when('test_result'); } return this._super.apply(this, arguments); }, intercepts: { do_action: function (event) { - assert.deepEqual(event.data.action, { - context: { - default_res_id: 2, - default_res_model: "partner", - default_previous_activity_type_id: 2, - }, - res_id: false, - res_model: 'mail.activity', - type: 'ir.actions.act_window', - target: "new", - view_mode: "form", - view_type: "form", - views: [[false, "form"]], - }, "should do a do_action with correct parameters"); - checkReadArgs = true; // should re-read the activities when closing the dialog + assert.strictEqual(event.data.action,'test_result' , "should do a do_action with correct parameters"); event.data.options.on_close(); }, }, diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index fabda64c269..fd5867e0648 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -57,6 +57,11 @@ + + + + + @@ -70,6 +75,7 @@ + diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 73f5cd4767e..1fac04fdca6 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -63,7 +63,7 @@ Tasks project.task - kanban,tree,form,calendar,pivot,graph + kanban,tree,form,calendar,pivot,graph,activity { 'search_default_project_id': [active_id], 'default_project_id': active_id, diff --git a/odoo/models.py b/odoo/models.py index 73c82944f5a..4a610a25176 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -1252,6 +1252,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): element = E.field(name=self._rec_name_fallback()) return E.tree(element, string=self._description) + @api.model + def _get_default_activity_view(self): + """ Generates an empty activity view. + + :returns: a activity view as an lxml document + :rtype: etree._Element + """ + return E.activity(string=self._description) + @api.model def _get_default_pivot_view(self): """ Generates an empty pivot view.
')) //empty cell for name + .append(_.map(this.state.data.activity_types, this._renderHeaderCell.bind(this))); + return $('
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 = $('
+ + + + + +