From 0bc2cbe149211f4be86db6f17f25d55065a1b1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 20 Dec 2016 16:25:42 +0100 Subject: [PATCH 01/15] [FIX] website_crm_partner_assign: fix selector use --- .../static/src/js/crm_partner_assign.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js b/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js index cfc158c46bc..ab42dac9690 100644 --- a/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js +++ b/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js @@ -37,7 +37,7 @@ $('.interested_partner_assign_confirm').on('click',function(e){ $('.desinterested_partner_assign_confirm').on('click',function(){ - var $btn = this; + var $btn = $(this); $btn.prop('disabled', true); new Model('crm.lead') .call("partner_desinterested", @@ -55,7 +55,7 @@ $('.desinterested_partner_assign_confirm').on('click',function(){ }); opp_stage_buttons.on('click',function(e){ - var $btn = this; + var $btn = $(this); $btn.prop('disabled', true); new Model('crm.lead') .call("write", [[parseInt(e.currentTarget.getAttribute('opp'))],{ @@ -76,7 +76,7 @@ $('.edit_contact_form .country_id').on('change', function(){ }); $('.edit_contact_confirm').on('click',function(){ - var $btn = this; + var $btn = $(this); $btn.prop('disabled', true); new Model('crm.lead') .call("write", [[parseInt($('.edit_contact_form .opportunity_id').val())],{ @@ -120,7 +120,7 @@ $('.new_opp_confirm').on('click',function(e){ }); $('.edit_opp_confirm').on('click',function(){ - var $btn = this; + var $btn = $(this); $btn.prop('disabled', true); new Model('crm.lead') .call("update_lead_portal", [[parseInt($('.edit_opp_form .opportunity_id').val())],{ From 4f54f13074796ab11f23f850e1c2b76ab67b51ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 29 Nov 2016 18:47:08 +0100 Subject: [PATCH 02/15] [FIX] mail: customers should not follow default internal subtypes This is a manual forward-port of commit 03ac47b9d71883a77600f0b416cb5150a07358aa Currently when someone is added as follow to a document default subtypes are computed if no specific subtypes are given to the subscription. However if internal subtypes are set as default those are added to all new followers. It should take into account customers and shared users and avoid adding them internal subtypes. Customers and shared users are not notified of messages using an internal subtype. So currently there is no information leak. However with this fix we ensure internal subtypes are not followed by external people even if no notification is created. Compared to original commit tests are slightly updated because there are now default internal subtypes in data. Not in 10.0 and previous versions. --- addons/mail/models/mail_followers.py | 7 ++++++- addons/mail/tests/test_mail_followers.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py index 304003fa4fc..9187917643a 100644 --- a/addons/mail/models/mail_followers.py +++ b/addons/mail/models/mail_followers.py @@ -66,11 +66,16 @@ class Followers(models.Model): default_subtypes = self.env['mail.message.subtype'].search([ ('default', '=', True), '|', ('res_model', '=', res_model), ('res_model', '=', False)]) + external_default_subtypes = default_subtypes.filtered(lambda subtype: not subtype.internal) if force_mode: + employee_pids = self.env['res.users'].sudo().search([('partner_id', 'in', partner_data.keys()), ('share', '=', False)]).mapped('partner_id').ids for pid, data in partner_data.iteritems(): if not data: - partner_data[pid] = default_subtypes.ids + if pid not in employee_pids: + partner_data[pid] = external_default_subtypes.ids + else: + partner_data[pid] = default_subtypes.ids for cid, data in channel_data.iteritems(): if not data: channel_data[cid] = default_subtypes.ids diff --git a/addons/mail/tests/test_mail_followers.py b/addons/mail/tests/test_mail_followers.py index db028ef3f65..c51978993bb 100644 --- a/addons/mail/tests/test_mail_followers.py +++ b/addons/mail/tests/test_mail_followers.py @@ -79,6 +79,22 @@ class TestMailFollowers(TestMail): self.assertEqual(len(follower), 1) self.assertEqual(follower.subtype_ids, self.default_group_subtypes) + def test_followers_subtypes_default_internal(self): + mt_mg_def_int = self.env['mail.message.subtype'].create({'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.channel', 'internal': True}) + self.group_pigs.message_subscribe_users(user_ids=[self.user_employee.id]) + follower = self.env['mail.followers'].search([ + ('res_model', '=', 'mail.channel'), + ('res_id', '=', self.group_pigs.id), + ('partner_id', '=', self.user_employee.partner_id.id)]) + self.assertEqual(follower.subtype_ids, self.default_group_subtypes | mt_mg_def_int) + + self.group_pigs.message_subscribe_users(user_ids=[self.user_portal.id]) + follower = self.env['mail.followers'].search([ + ('res_model', '=', 'mail.channel'), + ('res_id', '=', self.group_pigs.id), + ('partner_id', '=', self.user_portal.partner_id.id)]) + self.assertEqual(follower.subtype_ids, self.default_group_subtypes.filtered(lambda subtype: not subtype.internal)) + def test_followers_subtypes_specified(self): self.group_pigs.sudo(self.user_employee).message_subscribe_users(subtype_ids=[self.mt_mg_nodef.id]) self.assertEqual(self.group_pigs.message_follower_ids.mapped('partner_id'), self.user_employee.partner_id) From 936bc35820135d08432bbe6a9fd8201cf71c3944 Mon Sep 17 00:00:00 2001 From: Ravi Gadhia Date: Mon, 21 Nov 2016 11:35:20 +0100 Subject: [PATCH 03/15] [FIX] web_kanban: correctly align kanban status in kanban vignettes Otherwise ze bouboule is not aligned with other elements being in the same line of the vignette. --- addons/web_kanban/static/src/less/kanban_view.less | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/web_kanban/static/src/less/kanban_view.less b/addons/web_kanban/static/src/less/kanban_view.less index eccf02770b0..4e4cc8818e6 100644 --- a/addons/web_kanban/static/src/less/kanban_view.less +++ b/addons/web_kanban/static/src/less/kanban_view.less @@ -550,6 +550,7 @@ height: 12px; width: 12px; border-radius: 6px; + vertical-align: middle; background-color: @odoo-brand-secondary; From 60bacefd01437999c6c7f03a92a78df3857f39c1 Mon Sep 17 00:00:00 2001 From: Ravi Gadhia Date: Thu, 17 Nov 2016 12:54:15 +0100 Subject: [PATCH 04/15] [IMP] mail: add activities allowing to define next actions on various models This commit introduces generic activities to use in your addons. Activities are actions user have to take on a document like making a phonecall or organizing a meeting. Activities come with the mail module as they are integrated in the Chatter but are not bundled with mail.thread. New models are * mail.activity.type: used to categorize activities. Each type is a different kind of activity e.g. call, mail, meeting. An activity can be generic i.e. available for all models using activities; or specific to a model in which case res_model_id field should be used. * mail.activity: an actual activity to perform. Activities are linked to documents using res_id and res_model_id fields. Activities have a deadline that can be used in kanban view to display a status. Once done activities are unlinked and a message is posted. This message has a new activity_type_id field that indicates the activity linked to the message. This commit introduces a mail.activity mixin to use in various addons that enables the activities feature. It works like the mail.thread mixin. It defines a activity_ids one2many field toward activities using res_id and res_model_id. Various related / computed fields are also added to have a global status of activities on documents. Activities come with a new JS widget for the form view. It is integrated in the Chatter widget although it is a separate widget. It displays activities linked to the current record and allow to schedule, edit and mark done activities. Use widget="mail_activity" on activity_ids field in form view to use it. There is also a kanban widget defined. It defines a small widget to integrate in kanban vignettes. It allow to manage activities directly from the kanban view. Use widget="kanban_activity" on activitiy_ids field in kanban view to use it. Next commits will aim at integrating activities inside main Odoo addons. Thanks to R&D India for their work and testing on this task. Thanks to belgian Usabiliteam for reviewing and testing it. Thanks to @jem-odoo for the final review. May his soul lie in peace with the trumpets of paradise. --- addons/mail/__manifest__.py | 4 + addons/mail/data/mail_activity_data.xml | 27 +++ addons/mail/data/mail_data.xml | 6 + addons/mail/models/__init__.py | 1 + addons/mail/models/mail_activity.py | 226 ++++++++++++++++++ addons/mail/models/mail_message.py | 3 + addons/mail/security/ir.model.access.csv | 6 +- addons/mail/static/src/js/activity.js | 218 +++++++++++++++++ addons/mail/static/src/js/activity_log.js | 43 ++++ addons/mail/static/src/js/chatter.js | 26 +- .../mail/static/src/js/web_kanban_activity.js | 106 ++++++++ .../mail/static/src/less/mail_activity.less | 105 ++++++++ addons/mail/static/src/less/thread.less | 2 +- addons/mail/static/src/xml/activity.xml | 37 +++ addons/mail/static/src/xml/chatter.xml | 3 + .../static/src/xml/web_kanban_activity.xml | 60 +++++ addons/mail/views/mail_activity_views.xml | 141 +++++++++++ addons/mail/views/mail_templates.xml | 28 +++ addons/mail/views/res_config_views.xml | 12 + addons/mail/views/res_partner_views.xml | 13 + addons/mail/wizard/mail_compose_message.py | 1 + 21 files changed, 1063 insertions(+), 5 deletions(-) create mode 100644 addons/mail/data/mail_activity_data.xml create mode 100644 addons/mail/models/mail_activity.py create mode 100644 addons/mail/static/src/js/activity.js create mode 100644 addons/mail/static/src/js/activity_log.js create mode 100644 addons/mail/static/src/js/web_kanban_activity.js create mode 100644 addons/mail/static/src/less/mail_activity.less create mode 100644 addons/mail/static/src/xml/activity.xml create mode 100644 addons/mail/static/src/xml/web_kanban_activity.xml create mode 100644 addons/mail/views/mail_activity_views.xml diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 36fc7771e9f..bfc0d78008b 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -36,10 +36,12 @@ Main Features 'views/mail_followers_views.xml', 'views/mail_channel_views.xml', 'views/mail_shortcode_views.xml', + 'views/mail_activity_views.xml', 'views/res_config_views.xml', 'data/mail_data.xml', 'data/mail_channel_data.xml', 'data/mail_shortcode_data.xml', + 'data/mail_activity_data.xml', 'security/mail_security.xml', 'security/ir.model.access.csv', 'views/mail_alias_views.xml', @@ -57,6 +59,7 @@ Main Features 'installable': True, 'application': True, 'qweb': [ + 'static/src/xml/activity.xml', 'static/src/xml/client_action.xml', 'static/src/xml/composer.xml', 'static/src/xml/chatter.xml', @@ -66,5 +69,6 @@ Main Features 'static/src/xml/chat_window.xml', 'static/src/xml/extended_chat_window.xml', 'static/src/xml/announcement.xml', + 'static/src/xml/web_kanban_activity.xml', ], } diff --git a/addons/mail/data/mail_activity_data.xml b/addons/mail/data/mail_activity_data.xml new file mode 100644 index 00000000000..8e9a213dcff --- /dev/null +++ b/addons/mail/data/mail_activity_data.xml @@ -0,0 +1,27 @@ + + + + + Email + fa-envelope + Send an Email + + + Call + fa-phone + Make a Call + 2 + + + Meeting + fa-users + Reminder for meeting + + + Todo + fa-tasks + Task to perform + 5 + + + diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml index 4ca940fecd1..aa4cae02f30 100644 --- a/addons/mail/data/mail_data.xml +++ b/addons/mail/data/mail_data.xml @@ -56,6 +56,12 @@ + + Activities + + + + diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index 2426a6dc9e6..486b687eeb2 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -6,6 +6,7 @@ import mail_alias import mail_followers import mail_notification import mail_message +import mail_activity import mail_mail import mail_thread import mail_channel diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py new file mode 100644 index 00000000000..8f72110a8b9 --- /dev/null +++ b/addons/mail/models/mail_activity.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date, datetime, timedelta + +from odoo import api, fields, models + + +class MailActivityType(models.Model): + """ Activity Types are used to categorize activities. Each type is a different + kind of activity e.g. call, mail, meeting. An activity can be generic i.e. + available for all models using activities; or specific to a model in which + case res_model_id field should be used. """ + _name = 'mail.activity.type' + _description = 'Activity Type' + _rec_name = 'name' + _order = 'sequence' + + name = fields.Char('Name', required=True, translate=True) + summary = fields.Char('Summary', translate=True) + sequence = fields.Integer('Sequence', default=10) + days = fields.Integer( + '# Days', default=0, + help='Number of days before executing the action. It allows to plan the action deadline.') + icon = fields.Char('Icon', help="Font awesome icon e.g. fa-tasks") + res_model_id = fields.Many2one( + 'ir.model', 'Model', index=True, + help='Specify a model if the activity should be specific to a model' + 'and not available when managing activities for other models.') + + +class MailActivity(models.Model): + """ An actual activity to perform. Activities are linked to + documents using res_id and res_model_id fields. Activities have a deadline + that can be used in kanban view to display a status. Once done activities + are unlinked and a message is posted. This message has a new activity_type_id + field that indicates the activity linked to the message. """ + _name = 'mail.activity' + _description = 'Activity' + _order = 'date_deadline ASC' + _rec_name = 'summary' + + @api.model + def default_get(self, fields): + res = super(MailActivity, self).default_get(fields) + if not fields or 'res_model_id' in fields and res.get('res_model'): + res['res_model_id'] = self.env['ir.model'].sudo().search([('model', '=', res['res_model'])]).id + return res + + # owner + res_id = fields.Integer('Related Document ID', index=True, required=True) + res_model_id = fields.Many2one( + 'ir.model', 'Related Document Model', + index=True, ondelete='cascade', required=True) + res_model = fields.Char( + 'Related Document Model', + index=True, related='res_model_id.model', store=True, readonly=True) + res_name = fields.Char( + 'Document Name', compute='_compute_res_name', store=True, + help="Display name of the related document.", readonly=True) + # activity + activity_type_id = fields.Many2one( + 'mail.activity.type', 'Activity', + domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]") + icon = fields.Char('Icon', related='activity_type_id.icon') + summary = fields.Char('Summary') + note = fields.Html('Note') + date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.today) + # description + user_id = fields.Many2one( + 'res.users', 'Assigned to', + default=lambda self: self.env.user, + index=True, required=True) + state = fields.Selection([ + ('overdue', 'Overdue'), + ('today', 'Today'), + ('planned', 'Planned')], 'State', + compute='_compute_state') + + @api.depends('res_model', 'res_id') + def _compute_res_name(self): + for activity in self: + activity.res_name = self.env[activity.res_model].browse(activity.res_id).name_get()[0][1] + + @api.depends('date_deadline') + def _compute_state(self): + today = date.today() + for record in self.filtered(lambda activity: activity.date_deadline): + 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.onchange('activity_type_id') + def _onchange_activity_type_id(self): + if self.activity_type_id: + if not self.summary: + self.summary = self.activity_type_id.summary + self.date_deadline = (datetime.now() + timedelta(days=self.activity_type_id.days)) + + @api.model + def create(self, values): + activity = super(MailActivity, self).create(values) + activity.post_assignation_message() + return activity + + def write(self, values): + res = super(MailActivity, self).write(values) + if values.get('user_id'): + self.post_assignation_message() + return res + + def post_assignation_message(self): + for activity in self: + record = self.env[activity.res_model].browse(activity.res_id) + record.with_context( + mail_post_autofollow=True + ).message_post_with_view( + 'mail.message_activity_assigned', + values={'activity': activity}, + partner_ids=[activity.user_id.partner_id.id], + subtype_id=self.env.ref('mail.mt_note').id, + ) + + @api.multi + def action_done(self): + message = self.env['mail.message'] + for activity in self: + record = self.env[activity.res_model].browse(activity.res_id) + record.message_post_with_view( + 'mail.message_activity_done', + values={'activity': activity}, + subtype_id=self.env.ref('mail.mt_activities').id, + mail_activity_type_id=activity.activity_type_id.id, + ) + message |= record.message_ids[0] + + self.unlink() + return message.ids and message.ids[0] or False + + @api.multi + def action_close_dialog(self): + return {'type': 'ir.actions.act_window_close'} + + +class MailActivityMixin(models.AbstractModel): + """ Mail Activity Mixin is a mixin class to use if you want to add activities + management on a model. It works like the mail.thread mixin. It defines + an activity_ids one2many field toward activities using res_id and res_model_id. + Various related / computed fields are also added to have a global status of + activities on documents. + + Activities come with a new JS widget for the form view. It is integrated in the + Chatter widget although it is a separate widget. It displays activities linked + to the current record and allow to schedule, edit and mark done activities. + Use widget="mail_activity" on activity_ids field in form view to use it. + + There is also a kanban widget defined. It defines a small widget to integrate + in kanban vignettes. It allow to manage activities directly from the kanban + view. Use widget="kanban_activity" on activitiy_ids field in kanban view to + use it.""" + _name = 'mail.activity.mixin' + _description = 'Activity Mixin' + + activity_ids = fields.One2many( + 'mail.activity', 'res_id', 'Activities', + auto_join=True, + domain=lambda self: [('res_model', '=', self._name)]) + activity_state = fields.Selection([ + ('overdue', 'Overdue'), + ('today', 'Today'), + ('planned', 'Planned')], string='State', + compute='_compute_activity_state', + help='Status based on activities\nOverdue: Due date is already passed\n' + 'Today: Activity date is today\nPlanned: Future activities.') + activity_user_id = fields.Many2one( + 'res.users', 'Responsible', + related='activity_ids.user_id', + search='_search_activity_user_id') + activity_type_id = fields.Many2one( + 'mail.activity.type', 'Next Activity Type', + related='activity_ids.activity_type_id', + search='_search_activity_type_id') + activity_date_deadline = fields.Date( + 'Next Activity Deadline', related='activity_ids.date_deadline', + readonly=True, store=True) # store to enable ordering + search + activity_summary = fields.Char( + 'Next Activity Summary', related='activity_ids.summary', + search='_search_activity_summary') + + @api.depends('activity_ids.state') + def _compute_activity_state(self): + for record in self: + states = record.activity_ids.mapped('state') + if 'overdue' in states: + record.activity_state = 'overdue' + elif 'today' in states: + record.activity_state = 'today' + elif 'planned' in states: + record.activity_state = 'planned' + + @api.model + def _search_activity_user_id(self, operator, operand): + return [('activity_ids.user_id', operator, operand)] + + @api.model + def _search_activity_type_id(self, operator, operand): + return [('activity_ids.activity_type_id', operator, operand)] + + @api.model + def _search_activity_summary(self, operator, operand): + return [('activity_ids.summary', operator, operand)] + + @api.multi + def unlink(self): + """ Override unlink to delete records activities through (res_model, res_id). """ + record_ids = self.ids + result = super(MailActivityMixin, self).unlink() + self.env['mail.activity'].sudo().search( + [('res_model', '=', self._name), ('res_id', 'in', record_ids)] + ).unlink() + return result diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index a9751f7fd28..8fc1fae099b 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -61,6 +61,9 @@ class Message(models.Model): "message, comment for other messages such as user replies", oldname='type') subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', index=True) + mail_activity_type_id = fields.Many2one( + 'mail.activity.type', 'Mail Activity Type', + index=True, ondelete='set null') # origin email_from = fields.Char( 'From', default=_get_default_from, diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index e869287d2f4..79c264ccc98 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -33,4 +33,8 @@ access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,0 access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1 access_mail_shortcode,mail.shortcode,model_mail_shortcode,base.group_user,1,1,1,1 access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.group_portal,1,0,0,0 -access_mail_test_all,mail.test.all,model_mail_test,base.group_user,1,1,1,1 \ No newline at end of file +access_mail_test_all,mail.test.all,model_mail_test,base.group_user,1,1,1,1 +access_mail_activity_all,mail.activity.all,model_mail_activity,,1,0,0,0 +access_mail_activity_user,mail.activity.user,model_mail_activity,base.group_user,1,1,1,1 +access_mail_activity_type_all,mail.activity.type.all,model_mail_activity_type,,1,0,0,0 +access_mail_activity_type_user,mail.activity.type.user,model_mail_activity_type,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js new file mode 100644 index 00000000000..aa929445488 --- /dev/null +++ b/addons/mail/static/src/js/activity.js @@ -0,0 +1,218 @@ +odoo.define('mail.activity', function (require) { +"use strict"; + +var core = require('web.core'); +var form_common = require('web.form_common'); +var Model = require("web.Model"); +var Widget = require('web.Widget'); +var ChatManager = require('mail.chat_manager'); +var QWeb = core.qweb; +var _t = core._t; + + +/** + * Set the 'label_delay' entry in activity data according to the deadline date + * @param {Array} list of activity Object + * @return {Array} : list of modified activity Object + */ +var set_delay_label = function(activities){ + var today = moment().startOf('day'); + var today_str = today.format('YYYY-MM-DD'); + _.each(activities, function(activity){ + var to_display = ''; + var deadline = moment(activity.date_deadline + ' 00:00:00'); + var diff = deadline.diff(today, 'days', true); // true means no rounding + if(diff === 0){ + to_display = _t('Today'); + }else{ + if(diff < 0){ // overdue + if(diff === -1){ + to_display = _t('Yesterday'); + }else{ + to_display = _.str.sprintf(_t('%d days overdue'), Math.abs(diff)); + } + }else{ // due + if(diff === 1){ + to_display = _t('Tomorrow'); + }else{ + to_display = _.str.sprintf(_t('Due in %d days'), Math.abs(diff)); + } + } + } + activity['label_delay'] = to_display; + }); + return activities; +}; + +// ----------------------------------------------------------------------------- +// Activities Widget ('mail_activity' widget) +// +// Since it is displayed on a form view, it extends 'AbstractField' widget. +// +// Note: the activity widget is moved inside the chatter by the chatter itself +// for layout purposes. +// ----------------------------------------------------------------------------- + +var Activity = form_common.AbstractField.extend({ + className: 'o_mail_activity', + + events: { + "click .o_activity_edit": "on_activity_edit", + "click .o_activity_unlink": "on_activity_unlink", + "click .o_activity_done": "on_activity_done", + }, + + init: function () { + this._super.apply(this, arguments); + + this.context = this.options.context || {}; + this.activities = []; + this.Activity = new Model('mail.activity'); + }, + + start: function () { + var self = this; + + // Hide the chatter in 'create' mode + this.view.on("change:actual_mode", this, this.check_visibility); + + // find chatter if any + this.chatter = this.field_manager.fields.message_ids; + + return this._super(); + }, + + get_res_id: function () { + return this.view.datarecord.id; + }, + + check_visibility: function () { + this.set({"force_invisible": this.view.get("actual_mode") === "create"}); + }, + + set_value: function (_value) { + this.value = _value; + this._super(_value); + }, + + fetch_and_render_value: function () { + var self = this; + + return new Model(this.view.dataset.model) + .call("read", [[this.get_res_id()], ['activity_ids']]) + .then(function (results) { + self.value = results[0]['activity_ids']; + return self.render_value(); + }); + }, + + render_value: function () { + var self = this; + + return this.Activity + .call("read", [this.value]) + .then(function (results) { + this.activities = set_delay_label(results); + self.$el.html(QWeb.render('mail.activity_items', { + activities: this.activities, + })); + }); + }, + + on_activity_schedule: function (event) { + event.preventDefault(); + var self = this; + var action = { + type: 'ir.actions.act_window', + res_model: 'mail.activity', + view_mode: 'form', + view_type: 'form', + views: [ + [false, 'form'] + ], + target: 'new', + context: _.extend({ + default_res_id: this.get_res_id(), + default_res_model: this.view.dataset.model, + }, {'mark_done': true}, this.context), + res_id: false, + }; + return this.do_action(action, { + on_close: function() { + self.fetch_and_render_value(); + self.chatter.refresh_followers(); + ChatManager.get_messages({model: self.res_model, res_id: self.get_res_id()}); + }, + }); + }, + + on_activity_edit: function (event) { + event.preventDefault(); + var self = this; + var activity_id = this.$(event.currentTarget).data('activity-id'); + var action = { + type: 'ir.actions.act_window', + res_model: 'mail.activity', + view_mode: 'form', + view_type: 'form', + views: [ + [false, 'form'] + ], + target: 'new', + context: _.extend({ + default_res_id: this.get_res_id(), + default_res_model: this.model, + }, {'mark_done': true}, this.context), + res_id: activity_id, + } + this.do_action(action, { + on_close: function () { + self.fetch_and_render_value(); + self.chatter.refresh_followers(); + ChatManager.get_messages({model: self.res_model, res_id: self.get_res_id()}); + }, + }); + }, + + on_activity_unlink: function (event) { + event.preventDefault(); + var self = this; + var activity_id = this.$(event.currentTarget).data('activity-id'); + this.Activity + .call("unlink", [[activity_id]]) + .then(function (res) { + self.fetch_and_render_value(); + }); + }, + + on_activity_done: function (event) { + event.preventDefault(); + var self = this; + var activity_id = this.$(event.currentTarget).data('activity-id'); + this.Activity + .call("action_done", [[activity_id]]) + .then(function (msg_id) { + self.fetch_and_render_value(); + + self.chatter.msg_ids.unshift(msg_id); + + // to stop scrollbar flickering add min height of the thread and remove after + // render. on render thread it will remove and add all it's element which cause flickering + var thread = self.chatter.thread; + thread.$el.css('min-height', thread.$el.height()); + self.chatter.fetch_and_render_thread(self.chatter.msg_ids).then(function () { + thread.$el.css('min-height', ''); + }); + + }); + }, +}); + +core.form_widget_registry.add('mail_activity', Activity); + +return { + Activity: Activity, + set_delay_label: set_delay_label, +}; + +}); diff --git a/addons/mail/static/src/js/activity_log.js b/addons/mail/static/src/js/activity_log.js new file mode 100644 index 00000000000..a17f919ca89 --- /dev/null +++ b/addons/mail/static/src/js/activity_log.js @@ -0,0 +1,43 @@ +odoo.define('mail.activity.log', function (require) { +"use strict"; + +var Model = require("web.Model"); + + +var AbstractActivity = { + init: function(res_id, res_model){ + this.res_id = res_id; + this.res_model = res_model; + this.limit = 10; // TODO Add pager + this.Activity = new Model("mail.activity"); + }, + remove_activity_log: function (activity_id) { + return this.Activity.call("unlink", [activity_id]); + }, + mark_as_done: function (activity_id) { + return this.Activity.call("mark_as_done", [activity_id]); + }, + get_create_activity_action : function(activity_id){ + var context = activity_id ? {'mark_done': true} : {} + return { + type: 'ir.actions.act_window', + res_model: 'mail.activity', + view_mode: 'form', + view_type: 'form', + views: [ + [false, 'form'] + ], + target: 'new', + context: _.extend({ + default_res_id: this.res_id, + default_res_model: this.res_model, + }, context, this.context), + res_id: activity_id, + }; + }, +}; + +return { + AbstractActivity: AbstractActivity, +}; +}); diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index 8291237a2de..c55c04d71c4 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -10,7 +10,9 @@ var config = require('web.config'); var core = require('web.core'); var form_common = require('web.form_common'); var framework = require('web.framework'); +var Model = require("web.Model"); var web_utils = require('web.utils'); +var Widget = require('web.Widget'); var _t = core._t; var QWeb = core.qweb; @@ -278,8 +280,9 @@ var Chatter = form_common.AbstractField.extend({ template: 'mail.Chatter', events: { - "click .o_chatter_button_new_message": "on_open_composer_new_message", - "click .o_chatter_button_log_note": "on_open_composer_log_note", + 'click .o_chatter_button_new_message': 'on_open_composer_new_message', + 'click .o_chatter_button_log_note': 'on_open_composer_log_note', + 'click .o_activity_schedule': 'on_activity_schedule', }, init: function () { @@ -288,6 +291,10 @@ var Chatter = form_common.AbstractField.extend({ this.res_id = undefined; this.context = this.options.context || {}; this.dp = new web_utils.DropPrevious(); + // extends display options + this.options = _.extend(this.options || {}, { + 'display_activity_button': this.field_manager.fields.activity_ids + }); }, willStart: function () { @@ -315,6 +322,12 @@ var Chatter = form_common.AbstractField.extend({ this.followers.on('followers_update', this, this.on_followers_update); } + // Move the activities widget (if any) inside the chatter + this.activities = this.field_manager.fields.activity_ids + if (this.activities) { + this.$('.o_chatter_topbar').after(this.activities.$el); + } + this.thread = new ChatThread(this, { display_order: ChatThread.ORDER.DESC, display_document_link: false, @@ -476,6 +489,13 @@ var Chatter = form_common.AbstractField.extend({ this.followers.read_value(); } }, + + on_activity_schedule: function (event) { + if (this.activities) { + this.activities.on_activity_schedule(event); + } + }, + // composer toggle on_open_composer_new_message: function () { this.open_composer(); @@ -501,7 +521,7 @@ var Chatter = form_common.AbstractField.extend({ this.composer.on('input_focused', this, function () { this.composer.mention_set_prefetched_partners(this.mention_suggestions || []); }); - this.composer.insertBefore(this.$('.o_mail_thread')).then(function () { + this.composer.insertAfter(this.$('.o_chatter_topbar')).then(function () { // destroy existing composer if (old_composer) { old_composer.destroy(); diff --git a/addons/mail/static/src/js/web_kanban_activity.js b/addons/mail/static/src/js/web_kanban_activity.js new file mode 100644 index 00000000000..9e3f813142c --- /dev/null +++ b/addons/mail/static/src/js/web_kanban_activity.js @@ -0,0 +1,106 @@ +odoo.define('web.kanban.activity', function (require) { +"use strict"; + +var core = require('web.core'); +var Model = require("web.Model"); +var Widget = require('web.Widget'); +var kanban_widgets = require('web_kanban.widgets'); +var AbstractField = kanban_widgets.AbstractField; +var fields_registry = kanban_widgets.registry; +var set_delay_label = require('mail.activity').set_delay_label; + +var QWeb = core.qweb; + + +var KanbanActivity = AbstractField.extend({ + template: 'mail.KanbanActivity', + + events: { + "click .o_activity_btn": "get_activity_logs", + "click .o_schedule_activity": "on_activity_schedule", + "click .o_mark_as_done": "on_activity_done", + }, + + init: function(parent, field, $node) { + this._super.apply(this, arguments); + this.parent = parent; + this.activity_state = parent.record.activity_state.raw_value; + this.selection = _.object(parent.fields.activity_state.selection); + this.Activity = new Model("mail.activity"); + }, + + get_activity_logs: function(event) { + event.preventDefault(); + var self = this; + this.$('.o_activity').html(QWeb.render("mail.KanbanActivityLoading")); + this.Activity + .call("read", [this.field.raw_value]) + .then(function (records) { + records = set_delay_label(records); + self.$('.o_activity').html($(QWeb.render("mail.KanbanActivityDropdown", { + selection: self.selection, + records: _.groupBy(records, 'state'), + uid: self.session.uid, + }))); + }); + }, + + reset_state: function () { + var self = this; + new Model(self.parent.model).query(['activity_ids', 'activity_state']) + .filter([['id', '=', self.parent.id]]) + .first() + .then(function (record) { + var state_color_class = 'o_activity_color_' + (record.activity_state || 'default'); + var current_state_class = 'o_activity_color_' + (self.activity_state || 'default'); + self.$(".o_activity_btn > span").removeClass(current_state_class).addClass(state_color_class); + self.activity_state = record.activity_state; + self.parent.record.activity_ids.raw_value = record.activity_ids; + }); + }, + + on_activity_schedule: function(event) { + var self = this; + var activity_id = this.$(event.currentTarget).data('activity-id') || false; + var action = { + type: 'ir.actions.act_window', + res_model: 'mail.activity', + view_mode: 'form', + view_type: 'form', + views: [ + [false, 'form'] + ], + target: 'new', + context: _.extend({ + default_res_id: this.parent.id, + default_res_model: this.parent.model, + }, {'mark_done': true}, this.context), + res_id: activity_id, + }; + this.do_action(action, { + on_close: function() { + self.reset_state(); + self.get_activity_logs(event); + } + }); + }, + + on_activity_done: function (event) { + event.stopPropagation(); + var self = this; + var activity_id = this.$(event.currentTarget).data('activity-id'); + this.Activity + .call("action_done", [[activity_id]]) + .then(function (result) { + self.get_activity_logs(event); + self.reset_state(); + }); + }, +}); + +fields_registry.add('kanban_activity', KanbanActivity); + +return { + KanbanActivity: KanbanActivity +}; +}); diff --git a/addons/mail/static/src/less/mail_activity.less b/addons/mail/static/src/less/mail_activity.less new file mode 100644 index 00000000000..e4aa44ffd29 --- /dev/null +++ b/addons/mail/static/src/less/mail_activity.less @@ -0,0 +1,105 @@ +/* Common */ +.o_mail_activity { + &.o_form_field { + display: block; + } + + .o_thread_date_separator.o_border_dashed { + border-bottom: dashed #eeeeee 2px; + } + + .o_thread_message_core.o_mail_note { + padding: 0px 3px; + } + + .o_activity_color_default { + color: @gray; + } + + .o_activity_color_planned { + color: @brand-primary; + } + .o_activity_color_overdue { + color: @brand-danger; + } + .o_activity_color_today { + color: @brand-success; + } + + .o_activity_link { + color: @gray; + &:hover { + color: @brand-success; + } + } + +} + +/* Kanban View */ +.o_kanban_record { + .o_kanban_inline_block { + display: inline-block; + } + + .o_mail_activity { + .o_activity_btn { + span.fa { + vertical-align: -35%; + } + } + + .o_activity_link_kanban { + visibility: hidden; + } + + ul.o_activity { + width: 290px; + + & > li:last-child { + background-color: @odoo-brand-lightsecondary; + color: @gray-dark; + line-height: 3em; + } + li > ul.o_activity_log { + max-height: 300px; + overflow-y: auto; + color: @text-color; + + li { + width: 100%; + display: inline-block; + + &.o_activity_label { + line-height: 2.5em; + padding: 0px 5px; + background-color: @odoo-brand-lightsecondary; + } + + &.o_schedule_activity { + padding: 8px 10px 8px 0px; + vertical-align: middle; + &:hover { + background-color: @gray-lighter; + .o_activity_link_kanban { + visibility: visible; + } + } + + .o_activity_title { + width: 85%; + padding: 0px 5px 0px 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } + + .o_no_activity { + padding: 10px; + cursor: initial; + } + } + } +} diff --git a/addons/mail/static/src/less/thread.less b/addons/mail/static/src/less/thread.less index 7a9efa45731..0f2dd407575 100644 --- a/addons/mail/static/src/less/thread.less +++ b/addons/mail/static/src/less/thread.less @@ -3,7 +3,7 @@ @mail-thread-side-date-opacity: 0.6; @mail-thread-note: @gray-lighter; -.o_mail_thread { +.o_mail_thread, .o_mail_activity { .o_thread_show_more { text-align: center; } diff --git a/addons/mail/static/src/xml/activity.xml b/addons/mail/static/src/xml/activity.xml new file mode 100644 index 00000000000..0f699b6c2d1 --- /dev/null +++ b/addons/mail/static/src/xml/activity.xml @@ -0,0 +1,37 @@ + + + +
+ Planned +
+ +
+
+ + +
+
+
+ + + + planned by + + +
+ Edit | + Remove +
+
+
+ +
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/addons/mail/static/src/xml/chatter.xml b/addons/mail/static/src/xml/chatter.xml index 5073923f7c3..0adf4bc905f 100644 --- a/addons/mail/static/src/xml/chatter.xml +++ b/addons/mail/static/src/xml/chatter.xml @@ -59,6 +59,9 @@ + diff --git a/addons/mail/static/src/xml/web_kanban_activity.xml b/addons/mail/static/src/xml/web_kanban_activity.xml new file mode 100644 index 00000000000..e9529bbda69 --- /dev/null +++ b/addons/mail/static/src/xml/web_kanban_activity.xml @@ -0,0 +1,60 @@ + + + + + + + + +
  • + +
  • +
    + + +
  • + No activities planned. +
  • +
  • +
      + + +
    • + + () + +
    • +
    • +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
    • +
      +
    +
  • +
  • + Schedule an activity +
  • +
    + +
    diff --git a/addons/mail/views/mail_activity_views.xml b/addons/mail/views/mail_activity_views.xml new file mode 100644 index 00000000000..7295d45dae6 --- /dev/null +++ b/addons/mail/views/mail_activity_views.xml @@ -0,0 +1,141 @@ + + + + mail.activity.type.view.form + mail.activity.type + +
    + +
    +
    + + + + + + + + + + +
    +
    +
    +
    + + + mail.activity.type.view.tree + mail.activity.type + + + + + + + + + + + + + + Activities + mail.activity.type + form + tree,form + + + + mail.activity.view.form.popup + mail.activity + 20 + +
    + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    + + + mail.activity.view.search + mail.activity + + + + + + + + + + + + + + + + + + + + + + mail.activity.view.tree + mail.activity + + + + + + + + + + + + mail.activity.view.calendar + mail.activity + + + + + + + + + +
    diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index 97d9c834821..91f7be41d96 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -17,6 +17,8 @@ + + @@ -27,6 +29,7 @@ + @@ -50,5 +53,30 @@

    Powered by Odoo.

    + + + +
    diff --git a/addons/mail/views/res_config_views.xml b/addons/mail/views/res_config_views.xml index 230046f0c80..9bea2a0925b 100644 --- a/addons/mail/views/res_config_views.xml +++ b/addons/mail/views/res_config_views.xml @@ -21,6 +21,18 @@ + + + + diff --git a/addons/mail/views/res_partner_views.xml b/addons/mail/views/res_partner_views.xml index daaabf92657..b2cfa6ef131 100644 --- a/addons/mail/views/res_partner_views.xml +++ b/addons/mail/views/res_partner_views.xml @@ -31,6 +31,19 @@ + + + + + +
    diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 951b8a2b6b0..c58d86668d1 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -293,6 +293,7 @@ class MailComposer(models.TransientModel): 'record_name': self.record_name, 'no_auto_thread': self.no_auto_thread, 'mail_server_id': self.mail_server_id.id, + 'mail_activity_type_id': self.mail_activity_type_id.id, } # mass mailing: rendering override wizard static values if mass_mail_mode and self.model: From 4aa3f3f94eb094e18d7b381e0d0cdff0cc10ffb9 Mon Sep 17 00:00:00 2001 From: Ravi Gadhia Date: Mon, 21 Nov 2016 11:32:39 +0100 Subject: [PATCH 05/15] [IMP] account: add support of activities account.invoice model now supports activities. Filters have been added to ease their management. --- addons/account/models/account_invoice.py | 2 +- addons/account/views/account_invoice_view.xml | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 230d3e5df90..fe48cb54d1c 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -37,7 +37,7 @@ MAGIC_COLUMNS = ('id', 'create_uid', 'create_date', 'write_uid', 'write_date') class AccountInvoice(models.Model): _name = "account.invoice" - _inherit = ['mail.thread'] + _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Invoice" _order = "date_invoice desc, number desc, id desc" diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 40feb8caa75..16e36a1c04c 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -335,6 +335,7 @@
    +
    @@ -523,6 +524,18 @@ + + + + + From 87e457158e83295a17761a1ea0d251ba87472596 Mon Sep 17 00:00:00 2001 From: Ravi Gadhia Date: Mon, 21 Nov 2016 11:33:02 +0100 Subject: [PATCH 06/15] [IMP] crm, website_crm: replace support of activities crm.lead model now supports activities. Filters have been added to ease their management. Mail activities replace the old crm.activity model. All code related to crm.activity is then removed, including views and custom widget. Standard activities feature is now used widely in Odoo addons and replace those custom activities. A small update is required in website_crm_partner_assign. Now the portal user can only update or create its own activities aka assigned to him. If he has an activity assigned to him it is displayed in the opportunity website view. If not editing the opportunity will create a new activity assigned to him. --- addons/crm/__manifest__.py | 5 +- addons/crm/data/crm_activity_data.xml | 16 --- addons/crm/data/crm_activity_demo.xml | 37 ----- addons/crm/data/crm_lead_demo.xml | 132 +++++++++++++----- addons/crm/data/mail_activity_demo.xml | 24 ++++ addons/crm/models/__init__.py | 1 - addons/crm/models/crm_activity.py | 50 ------- addons/crm/models/crm_lead.py | 57 +++----- addons/crm/report/crm_activity_report.py | 6 +- .../crm/report/crm_activity_report_views.xml | 8 +- addons/crm/security/ir.model.access.csv | 3 - addons/crm/static/src/js/tour.js | 16 ++- addons/crm/tests/test_crm_activity.py | 126 +++++++---------- addons/crm/views/crm_activity_views.xml | 67 --------- addons/crm/views/crm_lead_views.xml | 88 ++++++------ addons/crm/wizard/__init__.py | 1 - addons/crm/wizard/crm_activity_log.py | 103 -------------- addons/crm/wizard/crm_activity_log_views.xml | 85 ----------- addons/crm/wizard/crm_lead_to_opportunity.py | 2 +- addons/mail/static/src/js/activity_log.js | 43 ------ .../static/src/xml/sales_team_dashboard.xml | 8 +- .../controllers/main.py | 9 +- .../models/crm_lead.py | 45 +++--- .../security/ir.model.access.csv | 1 - .../static/src/js/crm_partner_assign.js | 13 +- .../website_crm_partner_assign_templates.xml | 24 ++-- 26 files changed, 304 insertions(+), 666 deletions(-) delete mode 100644 addons/crm/data/crm_activity_data.xml delete mode 100644 addons/crm/data/crm_activity_demo.xml create mode 100644 addons/crm/data/mail_activity_demo.xml delete mode 100644 addons/crm/models/crm_activity.py delete mode 100644 addons/crm/views/crm_activity_views.xml delete mode 100644 addons/crm/wizard/crm_activity_log.py delete mode 100644 addons/crm/wizard/crm_activity_log_views.xml delete mode 100644 addons/mail/static/src/js/activity_log.js diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index b30f5ce5d15..ff62e0ca2a0 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -41,7 +41,6 @@ Dashboard for CRM will include: 'web_tour', ], 'data': [ - 'data/crm_activity_data.xml', 'data/crm_data.xml', 'data/crm_stage_data.xml', 'data/sales_config_settings_data.xml', @@ -53,7 +52,6 @@ Dashboard for CRM will include: 'security/ir.model.access.csv', 'wizard/base_partner_merge_views.xml', - 'wizard/crm_activity_log_views.xml', 'wizard/crm_lead_lost_views.xml', 'wizard/crm_lead_to_opportunity_views.xml', 'wizard/crm_merge_opportunities_views.xml', @@ -64,7 +62,6 @@ Dashboard for CRM will include: 'views/crm_templates.xml', 'views/crm_views.xml', - 'views/crm_activity_views.xml', 'views/crm_stage_views.xml', 'views/crm_lead_views.xml', 'views/calendar_views.xml', @@ -74,8 +71,8 @@ Dashboard for CRM will include: ], 'demo': [ 'data/crm_demo.xml', + 'data/mail_activity_demo.xml', 'data/crm_lead_demo.xml', - 'data/crm_activity_demo.xml', 'data/base_action_rule_demo.xml', ], 'css': ['static/src/css/crm.css'], diff --git a/addons/crm/data/crm_activity_data.xml b/addons/crm/data/crm_activity_data.xml deleted file mode 100644 index cb45e243e97..00000000000 --- a/addons/crm/data/crm_activity_data.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Email - - - Call - - - Task - - - - diff --git a/addons/crm/data/crm_activity_demo.xml b/addons/crm/data/crm_activity_demo.xml deleted file mode 100644 index 6c1bb4a25ba..00000000000 --- a/addons/crm/data/crm_activity_demo.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - Follow-up Quote - Follow-up quote - 30 - - - - Make Quote - Send a quotation - 15 - - - - - Call for Demo - 10 - Call for a demo - - - - - 5 - - - - - - 15 - - - - - diff --git a/addons/crm/data/crm_lead_demo.xml b/addons/crm/data/crm_lead_demo.xml index a4b40a6d984..e0d65f889c8 100644 --- a/addons/crm/data/crm_lead_demo.xml +++ b/addons/crm/data/crm_lead_demo.xml @@ -374,9 +374,6 @@ Andrew

    ]]>
    1 - - - Meeting to go over pricing information. @@ -402,9 +399,6 @@ Andrew

    ]]>
    2 - - - Send Catalog by Email @@ -427,9 +421,6 @@ Andrew

    ]]>
    1 - - - Call to get system requirements @@ -516,9 +507,6 @@ Andrew

    ]]>
    2 - - - Convert to quote @@ -542,9 +530,6 @@ Andrew

    ]]>
    1 - - - Send our service pricelist @@ -577,9 +562,6 @@ Andrew

    ]]>
    1 - - - Call to get training needs @@ -628,9 +610,6 @@ Andrew

    ]]>
    1 - - - Followup on the proposal @@ -653,7 +632,6 @@ Andrew

    ]]>
    +1 312 349 2324 2 - @@ -672,7 +650,6 @@ Andrew

    ]]>
    Cannon Hill Park 2 - @@ -691,7 +668,6 @@ Andrew

    ]]>
    1 - @@ -708,8 +684,6 @@ Andrew

    ]]>
    0 - - @@ -728,8 +702,6 @@ Andrew

    ]]>
    Lima 0 - - @@ -753,9 +725,6 @@ Andrew

    ]]>
    C1414CMS Capital Federal 2 - - - Conf call with technical service @@ -778,9 +747,6 @@ Andrew

    ]]>
    2 - - - Send Catalog by Email @@ -804,9 +770,6 @@ Andrew

    ]]>
    2 - - - Send Catalog by Email @@ -817,5 +780,100 @@ Andrew

    ]]>
    eval="[ref('crm_case_20'), ref('crm_case_23'), ref('crm_case_27')], {'install_mode': True}"/> + + + + + + Meeting to go over pricing information. + + + + + + + + Send Catalog by Email + + + + + + + Call to get system requirements + + + + + + + Convert to quote + + + + + + + Send our service pricelist + + + + + + + Call to get training needs + + + + + + + Followup on the proposal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Conf call with technical service + + + + + + + Send Catalog by Email + + diff --git a/addons/crm/data/mail_activity_demo.xml b/addons/crm/data/mail_activity_demo.xml new file mode 100644 index 00000000000..320c3b4baf2 --- /dev/null +++ b/addons/crm/data/mail_activity_demo.xml @@ -0,0 +1,24 @@ + + + + Follow-up Quote + fa-file-text-o + + Follow-up quote + 30 + + + Make Quote + fa-file-text-o + + Send a quotation + 15 + + + Call for Demo + fa-phone + + 10 + Call for a demo + + diff --git a/addons/crm/models/__init__.py b/addons/crm/models/__init__.py index 6edf901680d..02caa223da7 100644 --- a/addons/crm/models/__init__.py +++ b/addons/crm/models/__init__.py @@ -3,7 +3,6 @@ import res_users import calendar -import crm_activity import crm_lead import crm_stage import crm_team diff --git a/addons/crm/models/crm_activity.py b/addons/crm/models/crm_activity.py deleted file mode 100644 index 4be550c5564..00000000000 --- a/addons/crm/models/crm_activity.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models - - -class CrmActivity(models.Model): - ''' CrmActivity is a model introduced in Odoo v9 that models activities - performed in CRM, like phone calls, sending emails, making demonstrations, - ... Users are able to configure their custom activities. - - Each activity can configure recommended next activities. This allows to model - light custom workflows. This way sales manager can configure their crm - workflow that salepersons will use in their daily job. - - CrmActivity inherits from mail.message.subtype. This allows users to follow - some activities through subtypes. Each activity will generate messages with - the matching subtypes, allowing reporting and statistics computation based - on mail.message.subtype model. ''' - - _name = 'crm.activity' - _description = 'CRM Activity' - _inherits = {'mail.message.subtype': 'subtype_id'} - _rec_name = 'name' - _order = "sequence" - - days = fields.Integer('Number of days', default=0, - help='Number of days before executing the action, allowing you to plan the date of the action.') - sequence = fields.Integer('Sequence', default=0) - team_id = fields.Many2one('crm.team', string='Sales Team') - subtype_id = fields.Many2one('mail.message.subtype', string='Message Subtype', required=True, ondelete='cascade') - recommended_activity_ids = fields.Many2many( - 'crm.activity', 'crm_activity_rel', 'activity_id', 'recommended_id', - string='Recommended Next Activities') - preceding_activity_ids = fields.Many2many( - 'crm.activity', 'crm_activity_rel', 'recommended_id', 'activity_id', - string='Preceding Activities') - - # setting a default value on inherited fields is a bit involved - res_model = fields.Char('Model', related='subtype_id.res_model', inherited=True, default='crm.lead') - internal = fields.Boolean('Internal Only', related='subtype_id.internal', inherited=True, default=True) - default = fields.Boolean('Default', related='subtype_id.default', inherited=True, default=False) - - @api.multi - def unlink(self): - activities = self.search([('subtype_id', '=', self.subtype_id.id)]) - # to ensure that the subtype is only linked the current activity - if len(activities) == 1: - self.subtype_id.unlink() - return super(CrmActivity, self).unlink() diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 86651ef22c1..b2ed22ba7d4 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -52,11 +52,10 @@ CRM_LEAD_FIELDS_TO_MERGE = [ class Lead(FormatAddress, models.Model): - _name = "crm.lead" _description = "Lead/Opportunity" - _order = "priority desc,date_action,id desc" - _inherit = ['mail.thread', 'utm.mixin'] + _order = "priority desc,activity_date_deadline,id desc" + _inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin'] _mail_mass_mailing = _('Leads / Opportunities') def _default_probability(self): @@ -116,12 +115,6 @@ class Lead(FormatAddress, models.Model): probability = fields.Float('Probability', group_operator="avg", default=lambda self: self._default_probability()) planned_revenue = fields.Float('Expected Revenue', track_visibility='always') date_deadline = fields.Date('Expected Closing', help="Estimate of the date on which the opportunity will be won.") - - # CRM Actions - next_activity_id = fields.Many2one("crm.activity", string="Next Activity", index=True) - date_action = fields.Date('Next Activity Date', index=True) - title_action = fields.Char('Next Activity Summary') - color = fields.Integer('Color Index', default=0) partner_address_name = fields.Char('Partner Contact Name', related='partner_id.name', readonly=True) partner_address_email = fields.Char('Partner Contact Email', related='partner_id.email', readonly=True) @@ -170,8 +163,8 @@ class Lead(FormatAddress, models.Model): today = date.today() for lead in self: kanban_state = 'grey' - if lead.date_action: - lead_date = fields.Date.from_string(lead.date_action) + if lead.activity_date_deadline: + lead_date = fields.Date.from_string(lead.activity_date_deadline) if lead_date >= today: kanban_state = 'green' else: @@ -270,18 +263,6 @@ class Lead(FormatAddress, models.Model): if self.state_id: self.country_id = self.state_id.country_id.id - @api.onchange('next_activity_id') - def _onchange_next_activity_id(self): - values = { - 'title_action': False, - 'date_action': False, - } - if self.next_activity_id: - values['title_action'] = self.next_activity_id.description - if self.next_activity_id.days: - values['date_action'] = fields.Datetime.to_string(datetime.now() + timedelta(days=self.next_activity_id.days)) - self.update(values) - # ---------------------------------------- # ORM override (CRUD, fields_view_get, ...) # ---------------------------------------- @@ -934,13 +915,13 @@ class Lead(FormatAddress, models.Model): if date_deadline < date.today(): result['closing']['overdue'] += 1 # Next activities - if opp.next_activity_id and opp.date_action: - date_action = fields.Date.from_string(opp.date_action) - if date_action == date.today(): + for activity in opp.activity_ids: + date_deadline = fields.Date.from_string(activity.date_deadline) + if date_deadline == date.today(): result['activity']['today'] += 1 - if date.today() <= date_action <= date.today() + timedelta(days=7): + if date.today() <= date_deadline <= date.today() + timedelta(days=7): result['activity']['next_7_days'] += 1 - if date_action < date.today(): + if date_deadline < date.today(): result['activity']['overdue'] += 1 # Won in Opportunities if opp.date_closed: @@ -957,19 +938,19 @@ class Lead(FormatAddress, models.Model): # crm.activity is a very messy model so we need to do that in order to retrieve the actions done. self._cr.execute(""" SELECT - m.id, - m.subtype_id, - m.date, - l.user_id, - l.type - FROM mail_message M - LEFT JOIN crm_lead L ON (M.res_id = L.id) - INNER JOIN crm_activity A ON (M.subtype_id = A.subtype_id) + mail_message.id, + mail_message.subtype_id, + mail_message.mail_activity_type_id, + mail_message.date, + crm_lead.user_id, + crm_lead.type + FROM mail_message + LEFT JOIN crm_lead ON (mail_message.res_id = crm_lead.id) + INNER JOIN mail_activity_type activity_type ON (mail_message.mail_activity_type_id = activity_type.id) WHERE - (M.model = 'crm.lead') AND (L.user_id = %s) AND (L.type = 'opportunity') + (mail_message.model = 'crm.lead') AND (crm_lead.user_id = %s) AND (crm_lead.type = 'opportunity') """, (self._uid,)) activites_done = self._cr.dictfetchall() - for activity in activites_done: if activity['date']: date_act = fields.Date.from_string(activity['date']) diff --git a/addons/crm/report/crm_activity_report.py b/addons/crm/report/crm_activity_report.py index dd2dbc3925f..976886a3f5a 100644 --- a/addons/crm/report/crm_activity_report.py +++ b/addons/crm/report/crm_activity_report.py @@ -18,7 +18,8 @@ class ActivityReport(models.Model): team_id = fields.Many2one('crm.team', 'Sales Team', readonly=True) lead_id = fields.Many2one('crm.lead', "Lead", readonly=True) subject = fields.Char('Summary', readonly=True) - subtype_id = fields.Many2one('mail.message.subtype', 'Activity', readonly=True) + subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', readonly=True) + mail_activity_type_id = fields.Many2one('mail.activity.type', 'Activity Type', readonly=True) country_id = fields.Many2one('res.country', 'Country', readonly=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) stage_id = fields.Many2one('crm.stage', 'Stage', readonly=True) @@ -37,6 +38,7 @@ class ActivityReport(models.Model): select m.id, m.subtype_id, + m.mail_activity_type_id, m.author_id, m.date, m.subject, @@ -57,5 +59,5 @@ class ActivityReport(models.Model): on (m.res_id = l.id) WHERE - (m.model = 'crm.lead') + (m.model = 'crm.lead' AND m.mail_activity_type_id IS NOT NULL) )""") diff --git a/addons/crm/report/crm_activity_report_views.xml b/addons/crm/report/crm_activity_report_views.xml index 78c6c3f68b4..689d9fa01f4 100644 --- a/addons/crm/report/crm_activity_report_views.xml +++ b/addons/crm/report/crm_activity_report_views.xml @@ -6,7 +6,7 @@ crm.activity.report - + @@ -17,7 +17,7 @@ crm.activity.report - + @@ -28,7 +28,7 @@ crm.activity.report - + @@ -58,7 +58,7 @@ crm.activity.report form pivot,graph - {'search_default_my_activity': True} + {'search_default_activities_my': True} [] diff --git a/addons/crm/security/ir.model.access.csv b/addons/crm/security/ir.model.access.csv index 919f2c7a17d..b1490fb08e2 100644 --- a/addons/crm/security/ir.model.access.csv +++ b/addons/crm/security/ir.model.access.csv @@ -18,9 +18,6 @@ access_crm_lead_tag_manager,crm_lead_tag manager,model_crm_lead_tag,sales_team.g access_crm_lost_reason_manager,crm.lost.reason.manager,model_crm_lost_reason,sales_team.group_sale_manager,1,1,1,1 access_crm_lost_reason_salesman,crm.lost.reason.salesman,model_crm_lost_reason,sales_team.group_sale_salesman,1,0,0,0 access_crm_lost_reason_user,crm.lost.reason.user,model_crm_lost_reason,base.group_user,1,0,0,0 -access_crm_activity_user,crm.activity.user,model_crm_activity,base.group_user,1,1,1,0 -access_crm_activity_manager,crm.activity.manager,model_crm_activity,sales_team.group_sale_manager,1,1,1,1 -access_crm_activity_salesman,crm.activity.salesman,model_crm_activity,sales_team.group_sale_salesman,1,1,1,0 access_crm_activity_report_user,crm.activity.report.user,model_crm_activity_report,base.group_user,1,0,0,0 access_calendar_event_manager,calendar.event.manager,calendar.model_calendar_event,sales_team.group_sale_manager,1,1,1,1 access_calendar_event,calendar.event,calendar.model_calendar_event,sales_team.group_sale_salesman,1,1,1,0 diff --git a/addons/crm/static/src/js/tour.js b/addons/crm/static/src/js/tour.js index 4ad9b8093f2..7d27e6205d3 100644 --- a/addons/crm/static/src/js/tour.js +++ b/addons/crm/static/src/js/tour.js @@ -35,18 +35,22 @@ tour.register('crm_tour', { position: "right", run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ", }, { - trigger: ".o_kanban_record:not(.o_updating) .oe_kanban_status_grey", + trigger: ".o_kanban_record:not(.o_updating) .o_activity_color_default", extra_trigger: ".o_opportunity_kanban", - content: _t("This opportunity has no next activity scheduled. Click to set one."), + content: _t("This opportunity has no activity planned. Click to check them."), position: "bottom" }, { - trigger: ".o_recommended_activity .o_radio_item", + trigger: ".o_schedule_activity", + extra_trigger: ".o_opportunity_kanban", + content: _t("Schedule an activity by clicking here"), + position: "bottom" +}, { + trigger: ".modal-body .o_form_field_many2one", extra_trigger: ".o_opportunity_kanban", content: _t("

    You will be able to customize your followup activities. Examples:

    1. introductory email
    2. call 10 days after
    3. second call 3 days after, ...

    Select a standard activity for now on.

    "), - position: "left", + position: "bottom", run: function (actions) { - actions.auto(this.$anchor.children("input").first()); - actions.auto(".modal-footer .btn-primary"); + actions.auto(".modal-footer .btn-default"); }, }, { trigger: ".o_kanban_record", diff --git a/addons/crm/tests/test_crm_activity.py b/addons/crm/tests/test_crm_activity.py index 01747143ace..b18840e75f7 100644 --- a/addons/crm/tests/test_crm_activity.py +++ b/addons/crm/tests/test_crm_activity.py @@ -3,52 +3,47 @@ from .common import TestCrmCases from odoo import fields -from datetime import date +from datetime import datetime, timedelta -class TestCrmActivity(TestCrmCases): +class TestCrmMailActivity(TestCrmCases): def setUp(self): - super(TestCrmActivity, self).setUp() + super(TestCrmMailActivity, self).setUp() # Set up activities - Activity = self.env['crm.activity'] - self.activity3 = Activity.create({ + lead_model_id = self.env['ir.model'].search([('model', '=', 'crm.lead')]).id + ActivityType = self.env['mail.activity.type'] + self.activity3 = ActivityType.create({ 'name': 'Celebrate the sale', 'days': 3, - 'description': 'ACT 3 : Beers for everyone because I am a good salesman !', - 'internal': True, - 'res_model': 'crm.lead', + 'summary': 'ACT 3 : Beers for everyone because I am a good salesman !', + 'res_model_id': lead_model_id, }) - self.activity2 = Activity.create({ + self.activity2 = ActivityType.create({ 'name': 'Call for Demo', 'days': 6, - 'description': 'ACT 2 : I want to show you my ERP !', - 'internal': True, - 'res_model': 'crm.lead', - 'recommended_activity_ids': [(6, 0, [self.activity3.id])], + 'summary': 'ACT 2 : I want to show you my ERP !', + 'res_model_id': lead_model_id, }) - self.activity1 = Activity.create({ + self.activity1 = ActivityType.create({ 'name': 'Initial Contact', 'days': 5, - 'description': 'ACT 1 : Presentation, barbecue, ... ', - 'internal': True, - 'res_model': 'crm.lead', - 'recommended_activity_ids': [(6, 0, [self.activity2.id])], + 'summary': 'ACT 1 : Presentation, barbecue, ... ', + 'res_model_id': lead_model_id, }) # I create an opportunity, as salesman self.partner_client = self.env.ref("base.res_partner_1") - Lead = self.env['crm.lead'].sudo(self.crm_salesman.id) - self.lead = Lead.create({ + self.lead = self.env['crm.lead'].sudo(self.crm_salesman.id).create({ + 'name': 'Test Opp', 'type': 'opportunity', - 'name': 'Test Opportunity Activity Log', 'partner_id': self.partner_client.id, 'team_id': self.env.ref("sales_team.team_sales_department").id, 'user_id': self.crm_salesman.id, }) def test_crm_activity_recipients(self): - """ This test case check : + """ This test case checks - no internal subtype followed by client - activity subtype are not default ones - only activity followers are recipients when this kind of activity is logged @@ -60,73 +55,60 @@ class TestCrmActivity(TestCrmCases): self.lead.message_subscribe([self.partner_client.id]) # Check the client is not follower of any internal subtype - is_internal_subtype_for_client = self.lead.message_follower_ids.filtered(lambda fol: fol.partner_id.id == self.partner_client.id).mapped('subtype_ids.internal') - self.assertFalse(any(is_internal_subtype_for_client), 'Partner client is following an internal subtype') + internal_subtypes = self.lead.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_client).mapped('subtype_ids').filtered(lambda subtype: subtype.internal) + self.assertFalse(internal_subtypes) # Add sale manager as follower of default subtypes self.lead.message_subscribe([self.crm_salemanager.partner_id.id]) - # Make the sale manager follower of the activity subtype - manager_follower = self.env['mail.followers'].sudo().search([('res_model', '=', 'crm.lead'), ('res_id', '=', self.lead.id), ('partner_id', '=', self.crm_salemanager.partner_id.id)]) - manager_follower.write({ - 'subtype_ids': [(4, activity.subtype_id.id)] - }) - # Instanciate wizard, trigger onchange lead and set the next activity - ActivityLogWizard = self.env['crm.activity.log'].sudo(self.crm_salesman.id) - wizard = ActivityLogWizard.create({ + activity = self.env['mail.activity'].sudo(self.crm_salesman.id).create({ + 'activity_type_id': self.activity1.id, 'note': 'Content of the activity to log', - 'lead_id': self.lead.id, + 'res_id': self.lead.id, + 'res_model_id': self.env.ref('crm.model_crm_lead').id, }) - wizard.onchange_lead_id() - wizard.write({ - 'next_activity_id': activity.id, - }) - wizard.action_log() + activity._onchange_activity_type_id() + self.assertEqual(self.lead.activity_type_id, self.activity1) + self.assertEqual(self.lead.activity_summary, self.activity1.summary) + # self.assertEqual(self.lead.activity_date_deadline, self.activity1.summary) - # Check message recipients + # mark as done, check lead and posted message + activity.action_done() + self.assertFalse(self.lead.activity_type_id.id) + self.assertFalse(self.lead.activity_ids) activity_message = self.lead.message_ids[0] - self.assertEqual(activity_message.needaction_partner_ids, self.crm_salemanager.partner_id, 'Only the crm manager should be notified by the activity') - self.assertEqual(self.lead.next_activity_id.id, False, 'When logging activity, the next activity planned is erased') + self.assertEqual(activity_message.needaction_partner_ids, self.crm_salemanager.partner_id) + self.assertEqual(activity_message.subtype_id, self.env.ref('mail.mt_activities')) def test_crm_activity_next_action(self): """ This test case set the next activity on a lead, log another, and schedule a third. """ # Add the next activity (like we set it from a form view) - self.lead.write({ - 'next_activity_id': self.activity1.id + lead_model_id = self.env['ir.model'].search([('model', '=', 'crm.lead')]).id + activity = self.env['mail.activity'].sudo(self.crm_salesman.id).create({ + 'activity_type_id': self.activity1.id, + 'summary': 'My Own Summary', + 'res_id': self.lead.id, + 'res_model_id': lead_model_id, }) - self.lead._onchange_next_activity_id() + activity._onchange_activity_type_id() # Check the next activity is correct - self.assertEqual(self.lead.title_action, self.activity1.description, 'Activity title should be the same on the lead and on the chosen activity') + self.assertEqual(self.lead.activity_summary, activity.summary) + self.assertEqual(self.lead.activity_type_id, activity.activity_type_id) + # self.assertEqual(fields.Datetime.from_string(self.lead.activity_date_deadline), datetime.now() + timedelta(days=activity.activity_type_id.days)) - # Instanciate the wizard to schedule the next activity - wizard = self.env['crm.activity.log'].sudo(self.crm_salesman.id).create({ + activity.write({ + 'activity_type_id': self.activity2.id, + 'summary': '', 'note': 'Content of the activity to log', - 'lead_id': self.lead.id, }) - wizard.onchange_lead_id() - wizard.write({ - 'next_activity_id': self.activity2.id, - }) - wizard.action_log() + activity._onchange_activity_type_id() + + self.assertEqual(self.lead.activity_summary, activity.activity_type_id.summary) + self.assertEqual(self.lead.activity_type_id, activity.activity_type_id) + # self.assertEqual(fields.Datetime.from_string(self.lead.activity_date_deadline), datetime.now() + timedelta(days=activity.activity_type_id.days)) + + activity.action_done() # Check the next activity on the lead has been removed - self.assertFalse(self.lead.next_activity_id.id, 'No next activity should be set on lead, since we jsut log another activity') - - # Instanciate the wizard to schedule the next activity - self.env['crm.activity.log'].sudo(self.crm_salesman.id).create({ - 'next_activity_id': self.activity3.id, - 'note': 'Content of the activity to log', - 'lead_id': self.lead.id, - }) - wizard.onchange_lead_id() - wizard.write({ - 'next_activity_id': self.activity3.id, - }) - wizard.onchange_next_activity_id() - wizard.action_schedule() - - # Check the activity is well scheldule on lead - delta_days = (fields.Date.from_string(self.lead.date_action) - date.today()).days - self.assertEqual(self.activity3.days, delta_days, 'The action date should be in the number of days set up on the activity 3') - self.assertEqual(self.lead.title_action, self.activity3.description, 'Activity title should be the same on the lead and on the activity 3') + self.assertFalse(self.lead.activity_type_id) diff --git a/addons/crm/views/crm_activity_views.xml b/addons/crm/views/crm_activity_views.xml deleted file mode 100644 index 0950ccec890..00000000000 --- a/addons/crm/views/crm_activity_views.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - crm.activity.search - crm.activity - - - - - - - - - - crm.activity.form - crm.activity - -
    -
    -
    - - - - - - - - - - - -
    -
    -
    - - - crm.activity.tree - crm.activity - - - - - - - - - - - - Activities - crm.activity - form - tree,form - -

    Click to create a new activity.

    -
    -
    - - -
    diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 63772fd501d..f1c66cb726f 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -186,7 +186,7 @@ crm.lead - + @@ -240,16 +240,18 @@ - + - + + + @@ -284,16 +286,10 @@
    - - - - - - - +
    + +
    +
    @@ -332,6 +328,18 @@ + + + + + @@ -362,8 +370,6 @@ type="action" class="oe_highlight" context="{'default_lead_id': active_id}" attrs="{'invisible': [('active', '=', False),('probability', '<', 100)]}"/> -