#
##############################################################################
-from openerp.addons.base_status.base_stage import base_stage
import crm
from datetime import datetime
-from openerp.osv import fields, osv
-import time
+from operator import itemgetter
+
+import openerp
+from openerp import SUPERUSER_ID
from openerp import tools
-from openerp.tools.translate import _
+from openerp.addons.base.res.res_partner import format_address
+from openerp.osv import fields, osv, orm
from openerp.tools import html2plaintext
-
-from base.res.res_partner import format_address
+from openerp.tools.translate import _
CRM_LEAD_FIELDS_TO_MERGE = ['name',
'partner_id',
'company_id',
'country_id',
'section_id',
- 'stage_id',
'state_id',
+ 'stage_id',
'type_id',
'user_id',
'title',
'email_from',
'email_cc',
'partner_name']
-CRM_LEAD_PENDING_STATES = (
- crm.AVAILABLE_STATES[2][0], # Cancelled
- crm.AVAILABLE_STATES[3][0], # Done
- crm.AVAILABLE_STATES[4][0], # Pending
-)
-class crm_lead(base_stage, format_address, osv.osv):
+
+class crm_lead(format_address, osv.osv):
""" CRM Lead Case """
_name = "crm.lead"
_description = "Lead/Opportunity"
_order = "priority,date_action,id desc"
- _inherit = ['mail.thread','ir.needaction_mixin']
+ _inherit = ['mail.thread', 'ir.needaction_mixin']
+
+ _track = {
+ 'stage_id': {
+ # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
+ 'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.sequence <= 1,
+ 'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: (obj.stage_id and obj.stage_id.sequence > 1) and obj.probability < 100,
+ 'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj.probability == 100 and obj.stage_id and obj.stage_id.fold,
+ 'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.fold and obj.stage_id.sequence > 1,
+ },
+ }
+
+ def get_empty_list_help(self, cr, uid, help, context=None):
+ if context.get('default_type') == 'lead':
+ context['empty_list_help_model'] = 'crm.case.section'
+ context['empty_list_help_id'] = context.get('default_section_id')
+ context['empty_list_help_document_name'] = _("leads")
+ return super(crm_lead, self).get_empty_list_help(cr, uid, help, context=context)
def _get_default_section_id(self, cr, uid, context=None):
""" Gives default section by checking if present in the context """
- return (self._resolve_section_id_from_context(cr, uid, context=context) or False)
+ return self._resolve_section_id_from_context(cr, uid, context=context) or False
def _get_default_stage_id(self, cr, uid, context=None):
""" Gives default stage_id """
section_id = self._get_default_section_id(cr, uid, context=context)
- return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft')], context=context)
+ return self.stage_find(cr, uid, [], section_id, [('fold', '=', False)], context=context)
def _resolve_section_id_from_context(self, cr, uid, context=None):
""" Returns ID of section based on the value of 'section_id'
if type(context.get('default_section_id')) in (int, long):
return context.get('default_section_id')
if isinstance(context.get('default_section_id'), basestring):
- section_name = context['default_section_id']
- section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
+ section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
if len(section_ids) == 1:
return int(section_ids[0][0])
return None
stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
# restore order of the search
- result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
+ result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
fold = {}
for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
return result, fold
def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
- res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
+ if view_type == 'form' and context and context.get('opportunity_id'):
+ # TODO: replace by get_formview_action call
+ lead_type = self.browse(cr, user, context['opportunity_id'], context=context).type
+ view_lead_xml_id = 'crm_case_form_view_oppor' if lead_type == 'opportunity' else 'crm_case_form_view_leads'
+ _, view_id = self.pool['ir.model.data'].get_object_reference(cr, user, 'crm', view_lead_xml_id)
+ res = super(crm_lead, self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
if view_type == 'form':
res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
return res
res[lead.id][field] = abs(int(duration))
return res
- def _history_search(self, cr, uid, obj, name, args, context=None):
- res = []
- msg_obj = self.pool.get('mail.message')
- message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
- lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
-
- if lead_ids:
- return [('id', 'in', lead_ids)]
- else:
- return [('id', '=', '0')]
-
_columns = {
- 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
+ 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', track_visibility='onchange',
select=True, help="Linked partner (optional). Usually created when converting the lead."),
'id': fields.integer('ID', readonly=True),
'date_action_last': fields.datetime('Last Action', readonly=1),
'date_action_next': fields.datetime('Next Action', readonly=1),
'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
- 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
- select=True, tracked=True, help='When sending mails, the default email address is taken from the sales team.'),
- 'create_date': fields.datetime('Creation Date' , readonly=True),
- 'email_cc': fields.text('Global CC', size=252 , 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"),
+ 'section_id': fields.many2one('crm.case.section', 'Sales Team',
+ select=True, track_visibility='onchange', help='When sending mails, the default email address is taken from the sales team.'),
+ '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'),
- 'write_date': fields.datetime('Update Date' , readonly=True),
+ 'write_date': fields.datetime('Update Date', readonly=True),
'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
'contact_name': fields.char('Contact Name', size=64),
'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
- 'opt_out': fields.boolean('Opt-Out', oldname='optout', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
- 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
+ 'opt_out': fields.boolean('Opt-Out', oldname='optout',
+ help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
+ "Filter 'Available for Mass Mailing' allows users to filter the leads when performing mass mailing."),
+ 'type': fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', select=True, help="Type is used to separate Leads and Opportunities"),
'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
'date_closed': fields.datetime('Closed', readonly=True),
- 'stage_id': fields.many2one('crm.case.stage', 'Stage', tracked=True,
- domain="[('fold', '=', False), ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
- 'user_id': fields.many2one('res.users', 'Salesperson', tracked=True),
+ 'stage_id': fields.many2one('crm.case.stage', 'Stage', track_visibility='onchange', select=True,
+ domain="['&', ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
+ 'user_id': fields.many2one('res.users', 'Salesperson', select=True, track_visibility='onchange'),
'referred': fields.char('Referred By', size=64),
- 'date_open': fields.datetime('Opened', readonly=True),
+ 'date_open': fields.datetime('Assigned', readonly=True),
'day_open': fields.function(_compute_day, string='Days to Open', \
multi='day_open', type="float", store=True),
'day_close': fields.function(_compute_day, string='Days to Close', \
multi='day_close', type="float", store=True),
- 'state': fields.related('stage_id', 'state', type="selection", store=True,
- selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
- help='The Status is set to \'Draft\', when a case is created. If the case is in progress the Status is set to \'Open\'. When the case is over, the Status is set to \'Done\'. If the case needs to be reviewed then the Status is set to \'Pending\'.'),
+ 'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
+ # Messaging and marketing
+ 'message_bounce': fields.integer('Bounce'),
# Only used for type opportunity
- 'probability': fields.float('Success Rate (%)',group_operator="avg"),
- 'planned_revenue': fields.float('Expected Revenue', tracked=True),
- 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
- 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
+ 'probability': fields.float('Success Rate (%)', group_operator="avg"),
+ 'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
+ 'ref': fields.reference('Reference', selection=openerp.addons.base.res.res_request.referencable_models),
+ 'ref2': fields.reference('Reference 2', selection=openerp.addons.base.res.res_request.referencable_models),
'phone': fields.char("Phone", size=64),
'date_deadline': fields.date('Expected Closing', help="Estimate of the date on which the opportunity will be won."),
'date_action': fields.date('Next Action Date', select=True),
_defaults = {
'active': 1,
'type': 'lead',
- 'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
- 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
+ 'user_id': lambda s, cr, uid, c: uid,
'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
'color': 0,
+ 'date_last_stage_update': fields.datetime.now,
}
_sql_constraints = [
('check_probability', 'check(probability >= 0 and probability <= 100)', 'The probability of closing the deal should be between 0% and 100%!')
]
- def create(self, cr, uid, vals, context=None):
- obj_id = super(crm_lead, self).create(cr, uid, vals, context)
- section_id = self.browse(cr, uid, obj_id, context=context).section_id
- if section_id:
- followers = [follow.id for follow in section_id.message_follower_ids]
- self.message_subscribe(cr, uid, [obj_id], followers, context=context)
- self.create_send_note(cr, uid, [obj_id], context=context)
- return obj_id
-
def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
if not stage_id:
- return {'value':{}}
- stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
+ return {'value': {}}
+ stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context=context)
if not stage.on_change:
- return {'value':{}}
- return {'value':{'probability': stage.probability}}
+ return {'value': {}}
+ vals = {'probability': stage.probability}
+ if stage.probability >= 100 or (stage.probability == 0 and stage.sequence > 1):
+ vals['date_closed'] = fields.datetime.now()
+ return {'value': vals}
- def on_change_partner(self, cr, uid, ids, partner_id, context=None):
- result = {}
+ def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
values = {}
if partner_id:
partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
values = {
- 'partner_name' : partner.name,
- 'street' : partner.street,
- 'street2' : partner.street2,
- 'city' : partner.city,
- 'state_id' : partner.state_id and partner.state_id.id or False,
- 'country_id' : partner.country_id and partner.country_id.id or False,
- 'email_from' : partner.email,
- 'phone' : partner.phone,
- 'mobile' : partner.mobile,
- 'fax' : partner.fax,
+ 'partner_name': partner.name,
+ 'street': partner.street,
+ 'street2': partner.street2,
+ 'city': partner.city,
+ 'state_id': partner.state_id and partner.state_id.id or False,
+ 'country_id': partner.country_id and partner.country_id.id or False,
+ 'email_from': partner.email,
+ 'phone': partner.phone,
+ 'mobile': partner.mobile,
+ 'fax': partner.fax,
+ 'zip': partner.zip,
}
- return {'value' : values}
-
- def _check(self, cr, uid, ids=False, context=None):
- """ Override of the base.stage method.
- Function called by the scheduler to process cases for date actions
- Only works on not done and cancelled cases
- """
- cr.execute('select * from crm_case \
- where (date_action_last<%s or date_action_last is null) \
- and (date_action_next<=%s or date_action_next is null) \
- and state not in (\'cancel\',\'done\')',
- (time.strftime("%Y-%m-%d %H:%M:%S"),
- time.strftime('%Y-%m-%d %H:%M:%S')))
-
- ids2 = map(lambda x: x[0], cr.fetchall() or [])
- cases = self.browse(cr, uid, ids2, context=context)
- return self._action(cr, uid, cases, False, context=context)
+ return {'value': values}
+
+ def on_change_user(self, cr, uid, ids, user_id, context=None):
+ """ When changing the user, also set a section_id or restrict section id
+ to the ones user_id is member of. """
+ section_id = self._get_default_section_id(cr, uid, context=context) or False
+ if user_id and not section_id:
+ section_ids = self.pool.get('crm.case.section').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
+ if section_ids:
+ section_id = section_ids[0]
+ return {'value': {'section_id': section_id}}
def stage_find(self, cr, uid, cases, section_id, domain=None, order='sequence', context=None):
""" Override of the base.stage method
"""
if isinstance(cases, (int, long)):
cases = self.browse(cr, uid, cases, context=context)
+ if context is None:
+ context = {}
+ # check whether we should try to add a condition on type
+ avoid_add_type_term = any([term for term in domain if len(term) == 3 if term[0] == 'type'])
# collect all section_ids
- section_ids = []
+ section_ids = set()
types = ['both']
- if not cases :
- type = context.get('default_type')
- types += [type]
+ if not cases and context.get('default_type'):
+ ctx_type = context.get('default_type')
+ types += [ctx_type]
if section_id:
- section_ids.append(section_id)
+ section_ids.add(section_id)
for lead in cases:
if lead.section_id:
- section_ids.append(lead.section_id.id)
+ section_ids.add(lead.section_id.id)
if lead.type not in types:
types.append(lead.type)
# OR all section_ids and OR with case_default
search_domain.append(('section_ids', '=', section_id))
search_domain.append(('case_default', '=', True))
# AND with cases types
- search_domain.append(('type', 'in', types))
+ if not avoid_add_type_term:
+ search_domain.append(('type', 'in', types))
# AND with the domain in parameter
search_domain += list(domain)
# perform search, return the first found
- stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
+ stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, limit=1, context=context)
if stage_ids:
return stage_ids[0]
return False
- def case_cancel(self, cr, uid, ids, context=None):
- """ Overrides case_cancel from base_stage to set probability """
- res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
- self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
- return res
-
- def case_reset(self, cr, uid, ids, context=None):
- """ Overrides case_reset from base_stage to set probability """
- res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
- self.write(cr, uid, ids, {'probability': 0.0}, context=context)
- return res
-
def case_mark_lost(self, cr, uid, ids, context=None):
- """ Mark the case as lost: state=cancel and probability=0 """
- for lead in self.browse(cr, uid, ids):
- stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
+ """ Mark the case as lost: state=cancel and probability=0
+ :deprecated: this method will be removed in OpenERP v8.
+ """
+ stages_leads = {}
+ for lead in self.browse(cr, uid, ids, context=context):
+ stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0), ('fold', '=', True), ('sequence', '>', 1)], context=context)
if stage_id:
- self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
- self.case_mark_lost_send_note(cr, uid, ids, context=context)
+ if stages_leads.get(stage_id):
+ stages_leads[stage_id].append(lead.id)
+ else:
+ stages_leads[stage_id] = [lead.id]
+ else:
+ raise osv.except_osv(_('Warning!'),
+ _('To relieve your sales pipe and group all Lost opportunities, configure one of your sales stage as follow:\n'
+ 'probability = 0 %, select "Change Probability Automatically".\n'
+ 'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
+ for stage_id, lead_ids in stages_leads.items():
+ self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
return True
def case_mark_won(self, cr, uid, ids, context=None):
- """ Mark the case as lost: state=done and probability=100 """
- for lead in self.browse(cr, uid, ids):
- stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
+ """ Mark the case as won: state=done and probability=100
+ :deprecated: this method will be removed in OpenERP v8.
+ """
+ stages_leads = {}
+ for lead in self.browse(cr, uid, ids, context=context):
+ stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0), ('fold', '=', True)], context=context)
if stage_id:
- self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
- self.case_mark_won_send_note(cr, uid, ids, context=context)
+ if stages_leads.get(stage_id):
+ stages_leads[stage_id].append(lead.id)
+ else:
+ stages_leads[stage_id] = [lead.id]
+ else:
+ raise osv.except_osv(_('Warning!'),
+ _('To relieve your sales pipe and group all Won opportunities, configure one of your sales stage as follow:\n'
+ 'probability = 100 % and select "Change Probability Automatically".\n'
+ 'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
+ for stage_id, lead_ids in stages_leads.items():
+ self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
+ return True
+
+ def case_escalate(self, cr, uid, ids, context=None):
+ """ Escalates case to parent level """
+ for case in self.browse(cr, uid, ids, context=context):
+ data = {'active': True}
+ if case.section_id.parent_id:
+ data['section_id'] = case.section_id.parent_id.id
+ if case.section_id.parent_id.change_responsible:
+ if case.section_id.parent_id.user_id:
+ data['user_id'] = case.section_id.parent_id.user_id.id
+ else:
+ raise osv.except_osv(_('Error!'), _("You are already at the top level of your sales-team category.\nTherefore you cannot escalate furthermore."))
+ self.write(cr, uid, [case.id], data, context=context)
return True
- def set_priority(self, cr, uid, ids, priority):
+ def set_priority(self, cr, uid, ids, priority, context=None):
""" Set lead priority
"""
- return self.write(cr, uid, ids, {'priority' : priority})
+ return self.write(cr, uid, ids, {'priority': priority}, context=context)
def set_high_priority(self, cr, uid, ids, context=None):
""" Set lead priority to high
"""
- return self.set_priority(cr, uid, ids, '1')
+ return self.set_priority(cr, uid, ids, '1', context=context)
def set_normal_priority(self, cr, uid, ids, context=None):
""" Set lead priority to normal
"""
- return self.set_priority(cr, uid, ids, '3')
+ return self.set_priority(cr, uid, ids, '3', context=context)
def _merge_get_result_type(self, cr, uid, opps, context=None):
"""
opportunities = self.browse(cr, uid, ids, context=context)
def _get_first_not_null(attr):
- if hasattr(oldest, attr):
- return getattr(oldest, attr)
for opp in opportunities:
- if hasattr(opp, attr):
+ if hasattr(opp, attr) and bool(getattr(opp, attr)):
return getattr(opp, attr)
return False
return res and res.id or False
def _concat_all(attr):
- return ', '.join(filter(lambda x: x, [getattr(opp, attr) or '' for opp in opportunities if hasattr(opp, attr)]))
+ return '\n\n'.join(filter(lambda x: x, [getattr(opp, attr) or '' for opp in opportunities if hasattr(opp, attr)]))
# Process the fields' values
data = {}
# Define the resulting type ('lead' or 'opportunity')
data['type'] = self._merge_get_result_type(cr, uid, opportunities, context)
-
return data
- def _merge_find_oldest(self, cr, uid, ids, context=None):
- """
- Return the oldest lead found among ids.
-
- :param list ids: list of ids of the leads to inspect
- :return object: browse record of the oldest of the leads
- """
- if context is None:
- context = {}
-
- if context.get('convert'):
- ids = list(set(ids) - set(context.get('lead_ids', [])))
-
- # Search opportunities order by create date
- opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date', context=context)
- oldest_opp_id = opportunity_ids[0]
- return self.browse(cr, uid, oldest_opp_id, context=context)
-
def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
body = []
if title:
subject = [merge_message]
for opportunity in opportunities:
subject.append(opportunity.name)
- title = "%s : %s" % (merge_message, opportunity.name)
- details.append(self._mail_body(cr, uid, opportunity, CRM_LEAD_FIELDS_TO_MERGE, title=title, context=context))
+ title = "%s : %s" % (opportunity.type == 'opportunity' and _('Merged opportunity') or _('Merged lead'), opportunity.name)
+ fields = list(CRM_LEAD_FIELDS_TO_MERGE)
+ details.append(self._mail_body(cr, uid, opportunity, fields, title=title, context=context))
# Chatter message's subject
subject = subject[0] + ": " + ", ".join(subject[1:])
return True
def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
- attachment = self.pool.get('ir.attachment')
+ attach_obj = self.pool.get('ir.attachment')
# return attachments of opportunity
def _get_attachments(opportunity_id):
- attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
- return attachment.browse(cr, uid, attachment_ids, context=context)
+ attachment_ids = attach_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
+ return attach_obj.browse(cr, uid, attachment_ids, context=context)
- count = 1
first_attachments = _get_attachments(opportunity_id)
+ #counter of all attachments to move. Used to make sure the name is different for all attachments
+ count = 1
for opportunity in opportunities:
attachments = _get_attachments(opportunity.id)
- for first in first_attachments:
- for attachment in attachments:
- if attachment.name == first.name:
- values = dict(
- name = "%s (%s)" % (attachment.name, count,),
- res_id = opportunity_id,
- )
- attachment.write(values)
- count+=1
-
+ for attachment in attachments:
+ values = {'res_id': opportunity_id,}
+ for attachment_in_first in first_attachments:
+ if attachment.name == attachment_in_first.name:
+ name = "%s (%s)" % (attachment.name, count,),
+ count+=1
+ attachment.write(values)
return True
- def merge_opportunity(self, cr, uid, ids, context=None):
+ def merge_opportunity(self, cr, uid, ids, user_id=False, section_id=False, context=None):
"""
Different cases of merge:
- merge leads together = 1 new lead
:param list ids: leads/opportunities ids to merge
:return int id: id of the resulting lead/opp
"""
- if context is None: context = {}
+ if context is None:
+ context = {}
if len(ids) <= 1:
- raise osv.except_osv(_('Warning!'),_('Please select more than one element (lead or opportunity) from the list view.'))
-
- lead_ids = context.get('lead_ids', [])
+ raise osv.except_osv(_('Warning!'), _('Please select more than one element (lead or opportunity) from the list view.'))
- ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
opportunities = self.browse(cr, uid, ids, context=context)
- opportunities_list = list(set(opportunities) - set(ctx_opportunities))
- oldest = self._merge_find_oldest(cr, uid, ids, context=context)
- if ctx_opportunities:
- first_opportunity = ctx_opportunities[0]
- tail_opportunities = opportunities_list + ctx_opportunities[1:]
- else:
- first_opportunity = opportunities_list[0]
- tail_opportunities = opportunities_list[1:]
+ sequenced_opps = []
+ # Sorting the leads/opps according to the confidence level of its stage, which relates to the probability of winning it
+ # The confidence level increases with the stage sequence, except when the stage probability is 0.0 (Lost cases)
+ # An Opportunity always has higher confidence level than a lead, unless its stage probability is 0.0
+ for opportunity in opportunities:
+ sequence = -1
+ if opportunity.stage_id and not opportunity.stage_id.fold:
+ sequence = opportunity.stage_id.sequence
+ sequenced_opps.append(((int(sequence != -1 and opportunity.type == 'opportunity'), sequence, -opportunity.id), opportunity))
+
+ sequenced_opps.sort(reverse=True)
+ opportunities = map(itemgetter(1), sequenced_opps)
+ ids = [opportunity.id for opportunity in opportunities]
+ highest = opportunities[0]
+ opportunities_rest = opportunities[1:]
+
+ tail_opportunities = opportunities_rest
- merged_data = self._merge_data(cr, uid, ids, oldest, CRM_LEAD_FIELDS_TO_MERGE, context=context)
+ fields = list(CRM_LEAD_FIELDS_TO_MERGE)
+ merged_data = self._merge_data(cr, uid, ids, highest, fields, context=context)
+
+ if user_id:
+ merged_data['user_id'] = user_id
+ if section_id:
+ merged_data['section_id'] = section_id
# Merge messages and attachements into the first opportunity
- self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
- self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
+ self._merge_opportunity_history(cr, uid, highest.id, tail_opportunities, context=context)
+ self._merge_opportunity_attachments(cr, uid, highest.id, tail_opportunities, context=context)
# Merge notifications about loss of information
- self._merge_notify(cr, uid, first_opportunity, opportunities, context=context)
+ opportunities = [highest]
+ opportunities.extend(opportunities_rest)
+ self._merge_notify(cr, uid, highest.id, opportunities, context=context)
+ # Check if the stage is in the stages of the sales team. If not, assign the stage with the lowest sequence
+ if merged_data.get('section_id'):
+ section_stage_ids = self.pool.get('crm.case.stage').search(cr, uid, [('section_ids', 'in', merged_data['section_id']), ('type', '=', merged_data.get('type'))], order='sequence', context=context)
+ if merged_data.get('stage_id') not in section_stage_ids:
+ merged_data['stage_id'] = section_stage_ids and section_stage_ids[0] or False
# Write merged data into first opportunity
- self.write(cr, uid, [first_opportunity.id], merged_data, context=context)
- # Delete tail opportunities
- self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
+ self.write(cr, uid, [highest.id], merged_data, context=context)
+ # Delete tail opportunities
+ # We use the SUPERUSER to avoid access rights issues because as the user had the rights to see the records it should be safe to do so
+ self.unlink(cr, SUPERUSER_ID, [x.id for x in tail_opportunities], context=context)
- # Open first opportunity
- self.case_open(cr, uid, [first_opportunity.id])
- return first_opportunity.id
+ return highest.id
def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
crm_stage = self.pool.get('crm.case.stage')
contact_id = False
if customer:
contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
-
if not section_id:
section_id = lead.section_id and lead.section_id.id or False
-
- if section_id:
- stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
- else:
- stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
- stage_id = stage_ids and stage_ids[0] or False
-
- return {
+ val = {
'planned_revenue': lead.planned_revenue,
'probability': lead.probability,
'name': lead.name,
'partner_id': customer and customer.id or False,
'user_id': (lead.user_id and lead.user_id.id),
'type': 'opportunity',
- 'stage_id': stage_id or False,
'date_action': fields.datetime.now(),
'date_open': fields.datetime.now(),
'email_from': customer and customer.email or lead.email_from,
'phone': customer and customer.phone or lead.phone,
}
+ if not lead.stage_id or lead.stage_id.type=='lead':
+ val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('type', 'in', ('opportunity', 'both'))], context=context)
+ return val
def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
customer = False
partner = self.pool.get('res.partner')
customer = partner.browse(cr, uid, partner_id, context=context)
for lead in self.browse(cr, uid, ids, context=context):
- if lead.state in ('done', 'cancel'):
+ # TDE: was if lead.state in ('done', 'cancel'):
+ if lead.probability == 100 or (lead.probability == 0 and lead.stage_id.fold):
continue
vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
self.write(cr, uid, [lead.id], vals, context=context)
- self.convert_opportunity_send_note(cr, uid, lead, context=context)
if user_ids or section_id:
self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
partner = self.pool.get('res.partner')
- vals = { 'name': name,
+ vals = {'name': name,
'user_id': lead.user_id.id,
'comment': lead.description,
'section_id': lead.section_id.id or False,
'parent_id': parent_id,
'phone': lead.phone,
'mobile': lead.mobile,
- 'email': lead.email_from and tools.email_split(lead.email_from)[0],
+ 'email': tools.email_split(lead.email_from) and tools.email_split(lead.email_from)[0] or False,
'fax': lead.fax,
'title': lead.title and lead.title.id or False,
'function': lead.function,
'is_company': is_company,
'type': 'contact'
}
- partner = partner.create(cr, uid,vals, context)
+ partner = partner.create(cr, uid, vals, context=context)
return partner
def _create_lead_partner(self, cr, uid, lead, context=None):
- partner_id = False
+ partner_id = False
if lead.partner_name and lead.contact_name:
partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
elif not lead.partner_name and lead.contact_name:
partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
+ elif lead.email_from and self.pool.get('res.partner')._parse_partner_name(lead.email_from, context=context)[0]:
+ contact_name = self.pool.get('res.partner')._parse_partner_name(lead.email_from, context=context)[0]
+ partner_id = self._lead_create_contact(cr, uid, lead, contact_name, False, context=context)
else:
- partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
+ raise osv.except_osv(
+ _('Warning!'),
+ _('No customer name defined. Please fill one of the following fields: Company Name, Contact Name or Email ("Name <email@address>")')
+ )
return partner_id
- def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
- """
- Assign a partner to a lead.
-
- :param object lead: browse record of the lead to process
- :param int partner_id: identifier of the partner to assign
- :return bool: True if the partner has properly been assigned
- """
- res = False
- res_partner = self.pool.get('res.partner')
- if partner_id:
- res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
- contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
- res = lead.write({'partner_id': partner_id}, context=context)
- self._lead_set_partner_send_note(cr, uid, [lead.id], context)
- return res
-
def handle_partner_assignation(self, cr, uid, ids, action='create', partner_id=False, context=None):
"""
Handle partner assignation during a lead conversion.
"""
#TODO this is a duplication of the handle_partner_assignation method of crm_phonecall
partner_ids = {}
- # If a partner_id is given, force this partner for all elements
- force_partner_id = partner_id
for lead in self.browse(cr, uid, ids, context=context):
# If the action is set to 'create' and no partner_id is set, create a new one
- if action == 'create':
- partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context)
- self._lead_set_partner(cr, uid, lead, partner_id, context=context)
+ if lead.partner_id:
+ partner_ids[lead.id] = lead.partner_id.id
+ continue
+ if not partner_id and action == 'create':
+ partner_id = self._create_lead_partner(cr, uid, lead, context)
+ self.pool['res.partner'].write(cr, uid, partner_id, {'section_id': lead.section_id and lead.section_id.id or False})
+ if partner_id:
+ lead.write({'partner_id': partner_id}, context=context)
partner_ids[lead.id] = partner_id
return partner_ids
model_data = self.pool.get('ir.model.data')
phonecall_dict = {}
if not categ_id:
- res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
- if res_id:
+ try:
+ res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
+ except ValueError:
+ pass
for lead in self.browse(cr, uid, ids, context=context):
if not section_id:
section_id = lead.section_id and lead.section_id.id or False
'priority': lead.priority,
}
new_id = phonecall.create(cr, uid, vals, context=context)
- phonecall.case_open(cr, uid, [new_id], context=context)
+ phonecall.write(cr, uid, [new_id], {'state': 'open'}, context=context)
if action == 'log':
- phonecall.case_close(cr, uid, [new_id], context=context)
+ phonecall.write(cr, uid, [new_id], {'state': 'done'}, context=context)
phonecall_dict[lead.id] = new_id
self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
return phonecall_dict
'res_id': int(opportunity_id),
'view_id': False,
'views': [(form_view or False, 'form'),
- (tree_view or False, 'tree'),
- (False, 'calendar'), (False, 'graph')],
+ (tree_view or False, 'tree'),
+ (False, 'calendar'), (False, 'graph')],
'type': 'ir.actions.act_window',
}
:return dict: dictionary value for created Meeting view
"""
opportunity = self.browse(cr, uid, ids[0], context)
- res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
+ res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'calendar', 'action_calendar_event', context)
res['context'] = {
'default_opportunity_id': opportunity.id,
'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
}
return res
+ def create(self, cr, uid, vals, context=None):
+ if context is None:
+ context = {}
+ if vals.get('type') and not context.get('default_type'):
+ context['default_type'] = vals.get('type')
+ if vals.get('section_id') and not context.get('default_section_id'):
+ context['default_section_id'] = vals.get('section_id')
+
+ # context: no_log, because subtype already handle this
+ create_context = dict(context, mail_create_nolog=True)
+ return super(crm_lead, self).create(cr, uid, vals, context=create_context)
+
def write(self, cr, uid, ids, vals, context=None):
+ # stage change: update date_last_stage_update
+ if 'stage_id' in vals:
+ vals['date_last_stage_update'] = fields.datetime.now()
+ # stage change with new stage: update probability
if vals.get('stage_id') and not vals.get('probability'):
- # change probability of lead(s) if required by stage
- stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
- if stage.on_change:
- vals['probability'] = stage.probability
- if vals.get('section_id'):
- section_id = self.pool.get('crm.case.section').browse(cr, uid, vals.get('section_id'), context=context)
- if section_id:
- vals.setdefault('message_follower_ids', [])
- vals['message_follower_ids'] += [(4, follower.id) for follower in section_id.message_follower_ids]
- return super(crm_lead,self).write(cr, uid, ids, vals, context)
+ onchange_stage_values = self.onchange_stage_id(cr, uid, ids, vals.get('stage_id'), context=context)['value']
+ vals.update(onchange_stage_values)
+ return super(crm_lead, self).write(cr, uid, ids, vals, context=context)
+
+ def copy(self, cr, uid, id, default=None, context=None):
+ if not default:
+ default = {}
+ if not context:
+ context = {}
+ lead = self.browse(cr, uid, id, context=context)
+ local_context = dict(context)
+ local_context.setdefault('default_type', lead.type)
+ local_context.setdefault('default_section_id', lead.section_id)
+ if lead.type == 'opportunity':
+ default['date_open'] = fields.datetime.now()
+ else:
+ default['date_open'] = False
+ default['date_closed'] = False
+ default['stage_id'] = self._get_default_stage_id(cr, uid, local_context)
+ return super(crm_lead, self).copy(cr, uid, id, default, context=context)
# ----------------------------------------
# Mail Gateway
# ----------------------------------------
+ def message_get_reply_to(self, cr, uid, ids, context=None):
+ """ Override to get the reply_to of the parent project. """
+ return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False
+ for lead in self.browse(cr, SUPERUSER_ID, ids, context=context)]
+
+ def _get_formview_action(self, cr, uid, id, context=None):
+ action = super(crm_lead, self)._get_formview_action(cr, uid, id, context=context)
+ obj = self.browse(cr, uid, id, context=context)
+ if obj.type == 'opportunity':
+ model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
+ action.update({
+ 'views': [(view_id, 'form')],
+ })
+ return action
+
+ def message_get_suggested_recipients(self, cr, uid, ids, context=None):
+ recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context)
+ try:
+ for lead in self.browse(cr, uid, ids, context=context):
+ if lead.partner_id:
+ self._message_add_suggested_recipient(cr, uid, recipients, lead, partner=lead.partner_id, reason=_('Customer'))
+ elif lead.email_from:
+ self._message_add_suggested_recipient(cr, uid, recipients, lead, email=lead.email_from, reason=_('Customer Email'))
+ except (osv.except_osv, orm.except_orm): # no read access rights -> just ignore suggested recipients because this imply modifying followers
+ pass
+ return recipients
+
def message_new(self, cr, uid, msg, custom_values=None, context=None):
""" Overrides mail_thread message_new that is called by the mailgateway
through message_process.
This override updates the document according to the email.
"""
- if custom_values is None: custom_values = {}
-
- desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
- custom_values.update({
+ if custom_values is None:
+ custom_values = {}
+ defaults = {
'name': msg.get('subject') or _("No Subject"),
- 'description': desc,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
+ 'partner_id': msg.get('author_id', False),
'user_id': False,
- })
+ }
+ if msg.get('author_id'):
+ defaults.update(self.on_change_partner_id(cr, uid, None, msg.get('author_id'), context=context)['value'])
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
- custom_values['priority'] = msg.get('priority')
- return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+ defaults['priority'] = msg.get('priority')
+ defaults.update(custom_values)
+ return super(crm_lead, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
""" Overrides mail_thread message_update that is called by the mailgateway
# OpenChatter methods and notifications
# ----------------------------------------
- def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
- if isinstance(lead, (int, long)):
- lead = self.browse(cr, uid, [lead], context=context)[0]
- return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
-
- def create_send_note(self, cr, uid, ids, context=None):
- for id in ids:
- message = _("%s has been <b>created</b>.") % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_post(cr, uid, [id], body=message, context=context)
- return True
-
def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
- if action == 'log': prefix = 'Logged'
- else: prefix = 'Scheduled'
- message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
+ if action == 'log':
+ message = _('Logged a call for %(date)s. %(description)s')
+ else:
+ message = _('Scheduled a call for %(date)s. %(description)s')
+ phonecall_date = datetime.strptime(phonecall.date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ phonecall_usertime = fields.datetime.context_timestamp(cr, uid, phonecall_date, context=context).strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
+ html_time = "<time datetime='%s+00:00'>%s</time>" % (phonecall.date, phonecall_usertime)
+ message = message % dict(date=html_time, description=phonecall.description)
return self.message_post(cr, uid, ids, body=message, context=context)
- def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
- for lead in self.browse(cr, uid, ids, context=context):
- message = _("%s <b>partner</b> is now set to <em>%s</em>." % (self.case_get_note_msg_prefix(cr, uid, lead, context=context), lead.partner_id.name))
- lead.message_post(body=message)
- return True
-
- def convert_opportunity_send_note(self, cr, uid, lead, context=None):
- message = _("Lead has been <b>converted to an opportunity</b>.")
- lead.message_post(body=message)
- return True
+ def log_meeting(self, cr, uid, ids, meeting_subject, meeting_date, duration, context=None):
+ if not duration:
+ duration = _('unknown')
+ else:
+ duration = str(duration)
+ message = _("Meeting scheduled at '%s'<br> Subject: %s <br> Duration: %s hour(s)") % (meeting_date, meeting_subject, duration)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def onchange_state(self, cr, uid, ids, state_id, context=None):
if state_id: