diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py index 271422a60dd..04feb481db2 100644 --- a/addons/crm/__openerp__.py +++ b/addons/crm/__openerp__.py @@ -52,6 +52,7 @@ Dashboard for CRM will include: 'security/crm_security.xml', 'security/ir.model.access.csv', + 'wizard/crm_activity_log_views.xml', 'wizard/crm_lead_lost_view.xml', 'wizard/crm_lead_to_opportunity_view.xml', 'wizard/crm_merge_opportunities_view.xml', diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 590759ce1ec..8f046fc8cdb 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -127,6 +127,21 @@ class crm_lead(format_address, osv.osv): 'stage_id': _read_group_stage_ids } + def _compute_kanban_state(self, cr, uid, ids, fields, args, context=None): + """ Very interesting kanban state color. This makes complete sense. Or + not. """ + result = {} + today = datetime.now() + for lead in self.browse(cr, uid, ids, context=context): + result[lead.id] = 'normal' + if lead.date_action: + lead_date = datetime.strptime(lead.date_action, tools.DEFAULT_SERVER_DATE_FORMAT) + if lead_date > today: + result[lead.id] = 'green' + elif lead_date < today: + result[lead.id] = 'red' + return result + def _compute_day(self, cr, uid, ids, fields, args, context=None): """ :return dict: difference between current date and log date @@ -151,6 +166,7 @@ class crm_lead(format_address, osv.osv): duration = abs(int(ans.days)) res[lead.id][field] = duration return res + def _meeting_count(self, cr, uid, ids, field_name, arg, context=None): Event = self.pool['calendar.event'] return { @@ -170,6 +186,9 @@ class crm_lead(format_address, osv.osv): 'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1), 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id', select=True, track_visibility='onchange', help='When sending mails, the default email address is taken from the sales team.'), + 'kanban_state': fields.function( + _compute_kanban_state, string='Activity State', type="selection", + selection=[('grey', 'Normal'), ('red', 'Blocked'), ('green', 'Ready for next stage')]), 'create_date': fields.datetime('Creation Date', readonly=True), 'email_cc': fields.text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"), 'description': fields.text('Notes'), @@ -207,11 +226,7 @@ class crm_lead(format_address, osv.osv): '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 - 'last_activity_id': fields.many2one("crm.activity", "Last Activity", select=True), 'next_activity_id': fields.many2one("crm.activity", "Next Activity", select=True), - 'next_activity_1': fields.related("last_activity_id", "activity_1_id", "name", type="char", string="Next Activity 1"), - 'next_activity_2': fields.related("last_activity_id", "activity_2_id", "name", type="char", string="Next Activity 2"), - 'next_activity_3': fields.related("last_activity_id", "activity_3_id", "name", type="char", string="Next Activity 3"), 'date_action': fields.date('Next Activity Date', select=True), 'title_action': fields.char('Next Activity Summary'), @@ -365,62 +380,9 @@ class crm_lead(format_address, osv.osv): # Backward compatibility case_mark_won = action_set_won - def log_next_activity_1(self, cr, uid, ids, context=None): - return self.set_next_activity(cr, uid, ids, next_activity_name='activity_1_id', context=context) - - def log_next_activity_2(self, cr, uid, ids, context=None): - return self.set_next_activity(cr, uid, ids, next_activity_name='activity_2_id', context=context) - - def log_next_activity_3(self, cr, uid, ids, context=None): - return self.set_next_activity(cr, uid, ids, next_activity_name='activity_3_id', context=context) - - def set_next_activity(self, cr, uid, ids, next_activity_name, context=None): - for lead in self.browse(cr, uid, ids, context=context): - if not lead.last_activity_id: - continue - next_activity = next_activity_name and getattr(lead.last_activity_id, next_activity_name, False) or False - if next_activity: - date_action = False - if next_activity.days: - date_action = (datetime.now() + timedelta(days=next_activity.days)).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), - lead.write({ - 'next_activity_id': next_activity.id, - 'date_action': date_action, - 'title_action': next_activity.description, - }) - return True - - def log_next_activity_done(self, cr, uid, ids, context=None, next_activity_name=False): - to_clear_ids = [] - for lead in self.browse(cr, uid, ids, context=context): - if not lead.next_activity_id: - continue - body_html = """
Activity Done: ${object.next_activity_id.name}
-%if object.title_action: -
${object.title_action}
-%endif""" - body_html = self.pool['mail.template'].render_template(cr, uid, body_html, 'crm.lead', lead.id, context=context) - msg_id = lead.message_post(body_html, subtype_id=lead.next_activity_id.subtype_id.id) - to_clear_ids.append(lead.id) - self.write(cr, uid, [lead.id], {'last_activity_id': lead.next_activity_id.id}, context=context) - - if to_clear_ids: - self.cancel_next_activity(cr, uid, to_clear_ids, context=context) - return True - - def cancel_next_activity(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, { - 'next_activity_id': False, - 'date_action': False, - 'title_action': False, - }, context=context) - def onchange_next_activity_id(self, cr, uid, ids, next_activity_id, context=None): if not next_activity_id: return {'value': { - 'next_action1': False, - 'next_action2': False, - 'next_action3': False, 'title_action': False, 'date_action': False, }} @@ -429,12 +391,8 @@ class crm_lead(format_address, osv.osv): if activity.days: date_action = (datetime.now() + timedelta(days=activity.days)).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) return {'value': { - 'next_activity_1': activity.activity_1_id and activity.activity_1_id.name or False, - 'next_activity_2': activity.activity_2_id and activity.activity_2_id.name or False, - 'next_activity_3': activity.activity_3_id and activity.activity_3_id.name or False, 'title_action': activity.description, 'date_action': date_action, - 'last_activity_id': False, }} def _merge_get_result_type(self, cr, uid, opps, context=None): @@ -1234,12 +1192,10 @@ class crm_lead_tag(osv.Model): class crm_lost_reason(osv.Model): _name = "crm.lost.reason" _description = 'Reason for loosing leads' - _columns = { 'name': fields.char('Name', required=True), 'active': fields.boolean('Active'), } - _defaults = { 'active': True, } diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index 33031741f91..b7ee54d84ae 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -279,12 +279,15 @@ + + + @@ -312,22 +315,22 @@
- - -
-
- oe_kanban_text_red - - - : - - + ,
+
@@ -381,10 +384,6 @@ - - Opportunities @@ -400,6 +399,8 @@ type="action" class="oe_highlight" context="{'default_lead_id': active_id}" attrs="{'invisible': [('active', '=', False)]}"/> + - | - - | - - -
+
-
-
+ attrs="{'invisible': [('next_activity_id', '=', False)]}"/>
diff --git a/addons/crm/data/crm_action_demo.xml b/addons/crm/data/crm_action_demo.xml index 36570cf82db..6c1bb4a25ba 100644 --- a/addons/crm/data/crm_action_demo.xml +++ b/addons/crm/data/crm_action_demo.xml @@ -7,32 +7,28 @@ Follow-up quote 30
+ Make Quote Send a quotation 15 - + Call for Demo 10 Call for a demo - + - 5 + - - - Follow-up call - - - + 15 diff --git a/addons/crm/models/crm_activity.py b/addons/crm/models/crm_activity.py index b2f211d6fdb..ee6e7c89026 100644 --- a/addons/crm/models/crm_activity.py +++ b/addons/crm/models/crm_activity.py @@ -6,12 +6,12 @@ from openerp import api, fields, models class CrmActivity(models.Model): ''' CrmActivity is a model introduced in Odoo v9 that models activities - performed in CRM, like phonecalls, sending emails, making demonstrations, + performed in CRM, like phone calls, sending emails, making demonstrations, ... Users are able to configure their custom activities. - Each activity has up to three 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. + 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 @@ -29,9 +29,12 @@ class CrmActivity(models.Model): 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') - activity_1_id = fields.Many2one('crm.activity', string="Next Activity 1") - activity_2_id = fields.Many2one('crm.activity', string="Next Activity 2") - activity_3_id = fields.Many2one('crm.activity', string="Next Activity 3") + 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') @api.model def create(self, values): diff --git a/addons/crm/views/crm_action_views.xml b/addons/crm/views/crm_action_views.xml index 1d46749b652..d136ec27696 100644 --- a/addons/crm/views/crm_action_views.xml +++ b/addons/crm/views/crm_action_views.xml @@ -27,9 +27,7 @@ - - - + diff --git a/addons/crm/wizard/__init__.py b/addons/crm/wizard/__init__.py index e4c563df3a1..7f3fa1754f1 100644 --- a/addons/crm/wizard/__init__.py +++ b/addons/crm/wizard/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import crm_activity_log import crm_lead_lost import crm_partner_binding import crm_lead_to_opportunity diff --git a/addons/crm/wizard/crm_activity_log.py b/addons/crm/wizard/crm_activity_log.py new file mode 100644 index 00000000000..517993793fa --- /dev/null +++ b/addons/crm/wizard/crm_activity_log.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta + +from openerp import api, fields, models, tools, _ + + +class ActivityLog(models.TransientModel): + _name = "crm.activity.log" + _description = "Log an Activity" + _rec_name = 'title_action' + + next_activity_id = fields.Many2one('crm.activity', 'Activity') + last_activity_id = fields.Many2one('crm.activity', 'Previous Activity') + recommended_activity_id = fields.Many2one("crm.activity", "Recommended Activities") + title_action = fields.Char('Summary') + note = fields.Html('Note') + date_action = fields.Date('Next Activity Date') + lead_id = fields.Many2one( + 'crm.lead', 'Lead', required=True, + default=lambda self: self._context.get('default_lead_id', + self._context.get('active_model') == 'crm.lead' and self._context.get('active_id') or False)) + team_id = fields.Many2one( + 'crm.team', 'Sales Team') + date_deadline = fields.Date('Expected Closing') + planned_revenue = fields.Float('Expected Revenue') + + @api.onchange('lead_id') + def onchange_lead_id(self): + self.next_activity_id = self.lead_id.next_activity_id + self.date_deadline = self.lead_id.date_deadline + self.team_id = self.lead_id.team_id + self.planned_revenue = self.lead_id.planned_revenue + self.title_action = self.lead_id.title_action + + @api.onchange('next_activity_id') + def onchange_next_activity_id(self): + if not self.title_action: + self.title_action = self.next_activity_id.description + date_action = False + if self.next_activity_id and self.next_activity_id.days: + date_action = (datetime.now() + timedelta(days=self.next_activity_id.days)).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) + self.date_action = date_action + + @api.onchange('recommended_activity_id') + def onchange_recommended_activity_id(self): + self.next_activity_id = self.recommended_activity_id + + @api.multi + def action_log_and_schedule(self): + self.ensure_one() + self.action_log() + view_id = self.env.ref('crm.crm_activity_log_view_form_schedule') + return { + 'name': _('Next activity'), + 'res_model': 'crm.activity.log', + 'context': { + 'default_last_activity_id': self.next_activity_id.id, + 'default_lead_id': self.lead_id.id + }, + 'type': 'ir.actions.act_window', + 'view_id': False, + 'views': [(view_id.id, 'form')], + 'view_mode': 'form', + 'target': 'new', + 'view_type': 'form', + 'res_id': False + } + + @api.multi + def action_log(self): + for log in self: + body_html = "
%(title)s: %(next_activity)s
%(description)s%(note)s" % { + 'title': _('Activity Done'), + 'next_activity': log.next_activity_id.name, + 'description': log.title_action and '

%s

' % log.title_action or '', + 'note': log.note or '', + } + log.lead_id.message_post(body_html, subtype_id=log.next_activity_id.subtype_id.id) + log.lead_id.write({ + 'date_deadline': log.date_deadline, + 'planned_revenue': log.planned_revenue, + 'title_action': False, + 'date_action': False, + 'next_activity_id': False, + }) + return True + + @api.multi + def action_schedule(self): + for log in self: + log.lead_id.write({ + 'title_action': log.title_action, + 'date_action': log.date_action, + 'next_activity_id': log.next_activity_id.id, + }) + return True diff --git a/addons/crm/wizard/crm_activity_log_views.xml b/addons/crm/wizard/crm_activity_log_views.xml new file mode 100644 index 00000000000..73988958124 --- /dev/null +++ b/addons/crm/wizard/crm_activity_log_views.xml @@ -0,0 +1,74 @@ + + + + Log an Activity + crm.activity.log + 20 + +
+ + + + + + + + + + + + + +
+
+ +
+
+ + + Schedule next Activity + crm.activity.log + 40 + +
+ + + + + + + + + + +
+

Recommended activities:

+ +
+
+
+
+
+
+
+
+ + + Log an Activity + ir.actions.act_window + crm.activity.log + form + + new + +
\ No newline at end of file