X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=addons%2Fcrm%2Fcrm_lead.py;h=7680f8c49e5b3c96e123de77319760296958cde6;hb=c5bbcb470528685e05b3d5f02fd57682c3b6fe13;hp=e6924a7b2b67f13cf2cc5fe4a925887034f292a1;hpb=97c5a5cfcd647e079433887aa5da5bbb091815a4;p=odoo%2Fodoo.git diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index e6924a7..7680f8c 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). +# Copyright (C) 2004-today OpenERP SA () # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -19,15 +19,15 @@ # ############################################################################## -from osv import fields, osv -from datetime import datetime +import binascii +from base_status.base_stage import base_stage import crm +from datetime import datetime +from mail.mail_message import to_email +from osv import fields, osv import time -from tools.translate import _ -from crm import crm_case -import binascii import tools -from mail.mail_message import to_email +from tools.translate import _ CRM_LEAD_PENDING_STATES = ( crm.AVAILABLE_STATES[2][0], # Cancelled @@ -35,22 +35,69 @@ CRM_LEAD_PENDING_STATES = ( crm.AVAILABLE_STATES[4][0], # Pending ) -class crm_lead(crm_case, osv.osv): +class crm_lead(base_stage, osv.osv): """ CRM Lead Case """ _name = "crm.lead" _description = "Lead/Opportunity" _order = "priority,date_action,id desc" - _inherit = ['mail.thread','res.partner.address'] + _inherit = ['ir.needaction_mixin', 'mail.thread'] + _mail_compose_message = True + + 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) + + 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'), ('type', '=', 'both')], context=context) + + def _resolve_section_id_from_context(self, cr, uid, context=None): + """ Returns ID of section based on the value of 'section_id' + context key, or None if it cannot be resolved to a single + Sales Team. + """ + if context is None: + context = {} + 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) + if len(section_ids) == 1: + return int(section_ids[0][0]) + return None + + def _resolve_type_from_context(self, cr, uid, context=None): + """ Returns the type (lead or opportunity) from the type context + key. Returns None if it cannot be resolved. + """ + if context is None: + context = {} + return context.get('default_type') def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None): access_rights_uid = access_rights_uid or uid stage_obj = self.pool.get('crm.case.stage') order = stage_obj._order + # lame hack to allow reverting search, should just work in the trivial case if read_group_order == 'stage_id desc': - # lame hack to allow reverting search, should just work in the trivial case order = "%s desc" % order - stage_ids = stage_obj._search(cr, uid, ['|', ('id','in',ids),('case_default','=',1)], order=order, - access_rights_uid=access_rights_uid, context=context) + # retrieve section_id from the context and write the domain + # - ('id', 'in', 'ids'): add columns that should be present + # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded + # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded + search_domain = [] + section_id = self._resolve_section_id_from_context(cr, uid, context=context) + if section_id: + search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', False)] + search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)] + # retrieve type from the context (if set: choose 'type' or 'both') + type = self._resolve_type_from_context(cr, uid, context=context) + if type: + search_domain += ['|', ('type', '=', type), ('type', '=', 'both')] + # perform search + 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]))) @@ -60,14 +107,6 @@ class crm_lead(crm_case, osv.osv): 'stage_id': _read_group_stage_ids } - # overridden because res.partner.address has an inconvenient name_get, - # especially if base_contact is installed. - def name_get(self, cr, user, ids, context=None): - if isinstance(ids, (int, long)): - ids = [ids] - return [(r['id'], tools.ustr(r[self._rec_name])) - for r in self.read(cr, user, ids, [self._rec_name], context)] - def _compute_day(self, cr, uid, ids, fields, args, context=None): """ @param cr: the current row, from the database cursor, @@ -147,54 +186,53 @@ class crm_lead(crm_case, osv.osv): return res _columns = { - # Overridden from res.partner.address: 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', select=True, help="Optional linked partner, usually after conversion of the lead"), 'id': fields.integer('ID', readonly=True), - 'name': fields.char('Name', size=64, select=1), + 'name': fields.char('Subject', size=64, required=True, select=1), 'active': fields.boolean('Active', required=False), '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="E-mail address of the contact", select=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, 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"), 'description': fields.text('Notes'), 'write_date': fields.datetime('Update Date' , readonly=True), - 'categ_id': fields.many2one('crm.case.categ', 'Category', \ domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"), 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \ domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"), '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 that will be created while converting the lead into opportunity', select=1), - 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."), - 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."), + '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_in': fields.boolean('Opt-In', oldname='optin', help="If opt-in is checked, this contact has accepted to receive emails."), + '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"), 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True), 'date_closed': fields.datetime('Closed', readonly=True), - 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"), - 'user_id': fields.many2one('res.users', 'Salesman'), + 'stage_id': fields.many2one('crm.case.stage', 'Stage', + domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"), + 'user_id': fields.many2one('res.users', 'Salesperson'), 'referred': fields.char('Referred By', size=64), 'date_open': fields.datetime('Opened', 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.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True, - help='The state is set to \'Draft\', when a case is created.\ - \nIf the case is in progress the state is set to \'Open\'.\ - \nWhen the case is over, the state is set to \'Done\'.\ - \nIf the case needs to be reviewed then the state is set to \'Pending\'.'), - 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]), + 'state': fields.related('stage_id', 'state', type="selection", store=True, + selection=crm.AVAILABLE_STATES, string="State", readonly=True, + help='The state is set to \'Draft\', when a case is created.\ + If the case is in progress the state is set to \'Open\'.\ + When the case is over, the state is set to \'Done\'.\ + If the case needs to be reviewed then the state is \ + set to \'Pending\'.'), 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64), # Only used for type opportunity - 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"), - 'probability': fields.float('Probability (%)',group_operator="avg"), + 'probability': fields.float('Success Rate (%)',group_operator="avg"), 'planned_revenue': fields.float('Expected Revenue'), 'ref': fields.reference('Reference', selection=crm._links_get, size=128), 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128), @@ -202,23 +240,35 @@ class crm_lead(crm_case, osv.osv): 'date_deadline': fields.date('Expected Closing'), 'date_action': fields.date('Next Action Date', select=True), 'title_action': fields.char('Next Action', size=64), - 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('section_ids', '=', section_id)]"), 'color': fields.integer('Color Index'), - 'partner_address_name': fields.related('partner_address_id', 'name', type='char', string='Partner Contact Name', readonly=True), - 'partner_address_email': fields.related('partner_address_id', 'email', type='char', string='Partner Contact Email', readonly=True), + 'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True), + 'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True), 'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True), 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True), 'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True), - + + # Fields for address, due to separation from crm and res.partner + 'street': fields.char('Street', size=128), + 'street2': fields.char('Street2', size=128), + 'zip': fields.char('Zip', change_default=True, size=24), + 'city': fields.char('City', size=128), + 'state_id': fields.many2one("res.country.state", 'State', domain="[('country_id','=',country_id)]"), + 'country_id': fields.many2one('res.country', 'Country'), + 'phone': fields.char('Phone', size=64), + 'fax': fields.char('Fax', size=64), + 'mobile': fields.char('Mobile', size=64), + 'function': fields.char('Function', size=128), + 'title': fields.many2one('res.partner.title', 'Title'), + 'company_id': fields.many2one('res.company', 'Company', select=1), } _defaults = { - 'active': lambda *a: 1, - 'user_id': crm_case._get_default_user, - 'email_from': crm_case._get_default_email, - 'state': lambda *a: 'draft', - 'type': lambda *a: 'lead', - 'section_id': crm_case._get_section, + '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), + '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, @@ -228,167 +278,131 @@ class crm_lead(crm_case, osv.osv): obj_id = super(crm_lead, self).create(cr, uid, vals, context) self.create_send_note(cr, uid, [obj_id], context=context) return obj_id + + def on_change_opt_in(self, cr, uid, ids, opt_in): + return {'value':{'opt_in':opt_in,'opt_out':False}} - - def onchange_partner_address_id(self, cr, uid, ids, add, email=False): - """This function returns value of partner email based on Partner Address - """ - if not add: - return {'value': {'email_from': False, 'country_id': False}} - address = self.pool.get('res.partner.address').browse(cr, uid, add) - return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}} - - def on_change_optin(self, cr, uid, ids, optin): - return {'value':{'optin':optin,'optout':False}} - - def on_change_optout(self, cr, uid, ids, optout): - return {'value':{'optout':optout,'optin':False}} + def on_change_opt_out(self, cr, uid, ids, opt_out): + return {'value':{'opt_out':opt_out,'opt_in':False}} def onchange_stage_id(self, cr, uid, ids, stage_id, context={}): if not stage_id: return {'value':{}} stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context) + if stage.state == "draft": + return {'value':{'probability': 0.0}} + if stage.state == "open": + cases = self.browse(cr, uid, ids, context=context) + data = {'active': True} + for case in cases: + if case.stage_id and case.stage_id.state == 'draft': + data['date_open'] = fields.datetime.now() + if not case.user_id: + data['user_id'] = uid + return {'value':data} if not stage.on_change: return {'value':{}} - return {'value':{'probability': stage.probability}} + else: + return {'value':{'probability': stage.probability}} - def stage_find_percent(self, cr, uid, percent, section_id): - """ Return the first stage with a probability == percent + 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) + + def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None): + """ Override of the base.stage method + Parameter of the stage search taken from the lead: + - type: stage type must be the same or 'both' + - section_id: if set, stages must belong to this section or + be a default stage; if not set, stages must be default + stages """ - stage_pool = self.pool.get('crm.case.stage') - if section_id : - ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])]) - else : - ids = stage_pool.search(cr, uid, [("probability", '=', percent)]) - - if ids: - return ids[0] + if isinstance(cases, (int, long)): + cases = self.browse(cr, uid, cases, context=context) + # collect all section_ids + section_ids = [] + types = ['both'] + if section_id: + section_ids.append(section_id) + for lead in cases: + if lead.section_id: + section_ids.append(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 = [] + if section_ids: + search_domain += [('|')] * len(section_ids) + for section_id in section_ids: + search_domain.append(('section_ids', '=', section_id)) + search_domain.append(('case_default', '=', True)) + # AND with cases types + 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) + if stage_ids: + return stage_ids[0] return False - def stage_find_lost(self, cr, uid, section_id): - return self.stage_find_percent(cr, uid, 0.0, section_id) - - def stage_find_won(self, cr, uid, section_id): - return self.stage_find_percent(cr, uid, 100.0, section_id) - - def get_needaction_user_ids(self, cr, uid, ids, context=None): - result = dict.fromkeys(ids, []) - for obj in self.browse(cr, uid, ids, context=context): - # salesman must perform an action when in draft mode - if obj.state == 'draft' and obj.user_id: - result[obj.id] = [obj.user_id.id] - return result - - def message_get_subscribers(self, cr, uid, ids, context=None): - sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context) - # add salesman to the subscribers - for obj in self.browse(cr, uid, ids, context=context): - if obj.user_id: - sub_ids.append(obj.user_id.id) - return self.pool.get('res.users').read(cr, uid, sub_ids, context=context) - - def create_send_note(self, cr, uid, ids, context=None): - for id in ids: - message = _("%s has been created.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context)) - self.message_append_note(cr, uid, [id], _('System notification'), - message, type='notification', context=context) - return True - - def case_get_note_msg_prefix(self, cr, uid, id, context=None): - lead = self.browse(cr, uid, [id], context=context)[0] - return ('Opportunity' if lead.type == 'opportunity' else 'Lead') - - def case_mark_lost_send_note(self, cr, uid, ids, context=None): - message = _("Opportunity has been lost.") - return self.message_append_note(cr, uid, ids, message, context=context) - - def case_mark_won_send_note(self, cr, uid, ids, context=None): - message = _("Opportunity has been won.") - return self.message_append_note(cr, uid, ids, message, context=context) - - 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 = _("%s a call for the %s.") % (prefix, phonecall.date) - return self. message_append_note(cr, uid, ids, 'System Notification', 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 partner is now set to %s." % (self.case_get_note_msg_prefix(cr, uid, lead.id, context=context), lead.partner_id.name)) - lead.message_append_note('System Notification' ,message) - return True - - def case_open(self, cr, uid, ids, context=None): - for lead in self.browse(cr, uid, ids, context=context): - if lead.state == 'draft': - value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')} - self.write(cr, uid, [lead.id], value) - if lead.type == 'opportunity' and not lead.stage_id: - stage_id = self.stage_find(cr, uid, lead.section_id.id or False, [('sequence','>',0)]) - if stage_id: - self.stage_set(cr, uid, [lead.id], stage_id) - res = super(crm_lead, self).case_open(cr, uid, ids, context) - return res - - def case_close(self, cr, uid, ids, context=None): - res = super(crm_lead, self).case_close(cr, uid, ids, context) - self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')}) - return res - def case_cancel(self, cr, uid, ids, context=None): - """Overrides cancel for crm_case for setting probability - """ - res = super(crm_lead, self).case_cancel(cr, uid, ids, context) - self.write(cr, uid, ids, {'probability' : 0.0}) + """ 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 reset as draft in order to set the stage field as empty - """ - res = super(crm_lead, self).case_reset(cr, uid, ids, context) - self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0}) + """ 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 = done and probability = 0% - """ - res = super(crm_lead, self).case_close(cr, uid, ids, context) - self.write(cr, uid, ids, {'probability' : 0.0}) + """ Mark the case as lost: state=cancel and probability=0 """ for lead in self.browse(cr, uid, ids): - stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False) + stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context) if stage_id: - self.stage_set(cr, uid, [lead.id], stage_id) - return res + 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) + return True def case_mark_won(self, cr, uid, ids, context=None): - """Mark the case as lost: state = done and probability = 0% - """ - res = super(crm_lead, self).case_close(cr, uid, ids, context=None) - self.write(cr, uid, ids, {'probability' : 100.0}) + """ Mark the case as lost: state=done and probability=100 """ for lead in self.browse(cr, uid, ids): - stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False) + stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context) if stage_id: - self.stage_set(cr, uid, [lead.id], stage_id) - self.case_mark_won_send_note(cr, uid, [lead.id], context=context) - return res + 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) + return True def set_priority(self, cr, uid, ids, priority): - """Set lead priority + """ Set lead priority """ return self.write(cr, uid, ids, {'priority' : priority}) def set_high_priority(self, cr, uid, ids, context=None): - """Set lead priority to high + """ Set lead priority to high """ return self.set_priority(cr, uid, ids, '1') def set_normal_priority(self, cr, uid, ids, context=None): - """Set lead priority to normal + """ Set lead priority to normal """ return self.set_priority(cr, uid, ids, '3') - def _merge_data(self, cr, uid, ids, oldest, fields, context=None): # prepare opportunity data into dictionary for merging opportunities = self.browse(cr, uid, ids, context=context) @@ -476,7 +490,7 @@ class crm_lead(crm_case, osv.osv): subject = subject[0] + ", ".join(subject[1:]) details = "\n\n".join(details) - return self.message_append_note(cr, uid, [opportunity_id], subject, body=details) + return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details) def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None): message = self.pool.get('mail.message') @@ -537,8 +551,7 @@ class crm_lead(crm_case, osv.osv): first_opportunity = opportunities_list[0] tail_opportunities = opportunities_list[1:] - fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id', - 'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile', + fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile', 'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last', 'date_action_next', 'email_from', 'email_cc', 'partner_name'] @@ -582,14 +595,8 @@ class crm_lead(crm_case, osv.osv): 'stage_id': stage_id or False, 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'), 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), - 'partner_address_id': contact_id, } - def convert_opportunity_send_note(self, cr, uid, lead, context=None): - message = _("Lead has been converted to an opportunity.") - lead.message_append_note('' ,message) - return True - def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None): partner = self.pool.get('res.partner') mail_message = self.pool.get('mail.message') @@ -614,15 +621,42 @@ class crm_lead(crm_case, osv.osv): }, context=context) return True - def _lead_create_partner(self, cr, uid, lead, context=None): + def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None): partner = self.pool.get('res.partner') - partner_id = partner.create(cr, uid, { - 'name': lead.partner_name or lead.contact_name or lead.name, - 'user_id': lead.user_id.id, - 'comment': lead.description, - 'section_id': lead.section_id.id or False, - 'address': [] - }) + 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 to_email(lead.email_from)[0], + 'fax': lead.fax, + 'title': lead.title and lead.title.id or False, + 'function': lead.function, + 'street': lead.street, + 'street2': lead.street2, + 'zip': lead.zip, + 'city': lead.city, + 'country_id': lead.country_id and lead.country_id.id or False, + 'state_id': lead.state_id and lead.state_id.id or False, + 'is_company': is_company, + 'type': 'contact' + } + partner = partner.create(cr, uid,vals, context) + return partner + + def _create_lead_partner(self, cr, uid, lead, context=None): + 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) + self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context) + elif lead.partner_name and not lead.contact_name: + 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) + else: + partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context) return partner_id def _lead_set_partner(self, cr, uid, lead, partner_id, context=None): @@ -631,30 +665,10 @@ class crm_lead(crm_case, osv.osv): 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, 'partner_address_id': contact_id}, context=context) + res = lead.write({'partner_id' : partner_id, }, context=context) self._lead_set_partner_send_note(cr, uid, [lead.id], context) - return res - def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None): - address = self.pool.get('res.partner.address') - return address.create(cr, uid, { - 'partner_id': partner_id, - 'name': lead.contact_name, - 'phone': lead.phone, - 'mobile': lead.mobile, - 'email': lead.email_from and to_email(lead.email_from)[0], - 'fax': lead.fax, - 'title': lead.title and lead.title.id or False, - 'function': lead.function, - 'street': lead.street, - 'street2': lead.street2, - 'zip': lead.zip, - 'city': lead.city, - 'country_id': lead.country_id and lead.country_id.id or False, - 'state_id': lead.state_id and lead.state_id.id or False, - }) - def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None): """ This function convert partner based on action. @@ -667,8 +681,7 @@ class crm_lead(crm_case, osv.osv): for lead in self.browse(cr, uid, ids, context=context): if action == 'create': if not partner_id: - partner_id = self._lead_create_partner(cr, uid, lead, context=context) - self._lead_create_partner_address(cr, uid, lead, partner_id, context=context) + partner_id = self._create_lead_partner(cr, uid, lead, context) self._lead_set_partner(cr, uid, lead, partner_id, context=context) partner_ids[lead.id] = partner_id return partner_ids @@ -729,9 +742,8 @@ class crm_lead(crm_case, osv.osv): 'date' : schedule_time, 'section_id' : section_id or False, 'partner_id': lead.partner_id and lead.partner_id.id or False, - 'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False, - 'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False), - 'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False, + 'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False), + 'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False, 'priority': lead.priority, } new_id = phonecall.create(cr, uid, vals, context=context) @@ -763,128 +775,139 @@ class crm_lead(crm_case, osv.osv): 'type': 'ir.actions.act_window', } + def action_makeMeeting(self, cr, uid, ids, context=None): + """ This opens Meeting's calendar view to schedule meeting on current Opportunity + @return : 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['context'] = { + 'default_opportunity_id': opportunity.id, + 'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False, + 'default_user_id': uid, + 'default_section_id': opportunity.section_id and opportunity.section_id.id or False, + 'default_email_from': opportunity.email_from, + 'default_state': 'open', + 'default_name': opportunity.name, + } + return res + + def unlink(self, cr, uid, ids, context=None): + for lead in self.browse(cr, uid, ids, context): + if (not lead.section_id.allow_unlink) and (lead.state != 'draft'): + raise osv.except_osv(_('Error'), + _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \ + "You should better cancel it, instead of deleting it.") % lead.name) + return super(crm_lead, self).unlink(cr, uid, ids, context) + + def write(self, cr, uid, ids, vals, context=None): + 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 + return super(crm_lead,self).write(cr, uid, ids, vals, context) + + # ---------------------------------------- + # Mail Gateway + # ---------------------------------------- def message_new(self, cr, uid, msg, custom_values=None, context=None): - """Automatically calls when new email message arrives""" - res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context) - subject = msg.get('subject') or _("No Subject") - body = msg.get('body_text') - - msg_from = msg.get('from') - priority = msg.get('priority') - vals = { - 'name': subject, - 'email_from': msg_from, + """ 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 = {} + custom_values.update({ + 'name': msg.get('subject') or _("No Subject"), + 'description': msg.get('body_text'), + 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), - 'description': body, 'user_id': False, - } - if priority: - vals['priority'] = priority - vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False))) - self.write(cr, uid, [res_id], vals, context) - return res_id - - def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None): + }) + if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES): + custom_values['priority'] = msg.get('priority') + custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context)) + return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, 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 + through message_process. + This method updates the document according to the email. + """ if isinstance(ids, (str, int, long)): ids = [ids] - if vals == None: - vals = {} - super(crm_lead, self).message_update(cr, uid, ids, msg, context=context) + if update_vals is None: update_vals = {} if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES): vals['priority'] = msg.get('priority') maps = { 'cost':'planned_cost', 'revenue': 'planned_revenue', - 'probability':'probability' + 'probability':'probability', } - vls = {} - for line in msg['body_text'].split('\n'): + for line in msg.get('body_text', '').split('\n'): line = line.strip() res = tools.misc.command_re.match(line) if res and maps.get(res.group(1).lower()): key = maps.get(res.group(1).lower()) - vls[key] = res.group(2).lower() - vals.update(vls) - - # Unfortunately the API is based on lists - # but we want to update the state based on the - # previous state, so we have to loop: - for case in self.browse(cr, uid, ids, context=context): - values = dict(vals) - if case.state in CRM_LEAD_PENDING_STATES: - #re-open - values.update(state=crm.AVAILABLE_STATES[1][0]) - if not case.date_open: - values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) - res = self.write(cr, uid, [case.id], values, context=context) - return res + vals[key] = res.group(2).lower() - def action_makeMeeting(self, cr, uid, ids, context=None): - """ - This opens Meeting's calendar view to schedule meeting on current Opportunity - @return : Dictionary value for created Meeting view - """ - if context is None: - context = {} - value = {} - data_obj = self.pool.get('ir.model.data') - for opp in self.browse(cr, uid, ids, context=context): - # Get meeting views - tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet') - form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet') - calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet') - search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter') - context.update({ - 'default_opportunity_id': opp.id, - 'default_partner_id': opp.partner_id and opp.partner_id.id or False, - 'default_user_id': uid, - 'default_section_id': opp.section_id and opp.section_id.id or False, - 'default_email_from': opp.email_from, - 'default_state': 'open', - 'default_name': opp.name - }) - value = { - 'name': _('Meetings'), - 'context': context, - 'view_type': 'form', - 'view_mode': 'calendar,form,tree', - 'res_model': 'crm.meeting', - 'view_id': False, - 'views': [(calander_view and calander_view[1] or False, 'calendar'), (form_view and form_view[1] or False, 'form'), (tree_view and tree_view[1] or False, 'tree')], - 'type': 'ir.actions.act_window', - 'search_view_id': search_view and search_view[1] or False, - 'nodestroy': True - } - return value + return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context) + # ---------------------------------------- + # OpenChatter methods and notifications + # ---------------------------------------- - def unlink(self, cr, uid, ids, context=None): - for lead in self.browse(cr, uid, ids, context): - if (not lead.section_id.allow_unlink) and (lead.state != 'draft'): - raise osv.except_osv(_('Error'), - _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \ - "You should better cancel it, instead of deleting it.") % lead.name) - return super(crm_lead, self).unlink(cr, uid, ids, context) + def message_get_subscribers(self, cr, uid, ids, context=None): + """ Override to add the salesman. """ + user_ids = super(crm_lead, self).message_get_subscribers(cr, uid, ids, context=context) + for obj in self.browse(cr, uid, ids, context=context): + if obj.user_id and not obj.user_id.id in user_ids: + user_ids.append(obj.user_id.id) + return user_ids + + def stage_set_send_note(self, cr, uid, ids, stage_id, context=None): + """ Override of the (void) default notification method. """ + stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1] + return self.message_append_note(cr, uid, ids, body= _("Stage changed to %s.") % (stage_name), context=context) + + 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 created.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context)) + self.message_append_note(cr, uid, [id], body=message, context=context) + return True + def case_mark_lost_send_note(self, cr, uid, ids, context=None): + message = _("Opportunity has been lost.") + return self.message_append_note(cr, uid, ids, body=message, context=context) - def write(self, cr, uid, ids, vals, context=None): - if not context: - context = {} + def case_mark_won_send_note(self, cr, uid, ids, context=None): + message = _("Opportunity has been won.") + return self.message_append_note(cr, uid, ids, body=message, context=context) - if 'date_closed' in vals: - return super(crm_lead,self).write(cr, uid, ids, vals, context=context) + 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 = _("%s a call for the %s.") % (prefix, phonecall.date) + return self.message_append_note(cr, uid, ids, body=message, context=context) - if vals.get('stage_id'): - stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context) - # change probability of lead(s) if required by stage - if not vals.get('probability') and stage.on_change: - vals['probability'] = stage.probability - for case in self.browse(cr, uid, ids, context=context): - message = _("Stage changed to %s.") % (stage.name) - case.message_append_note('', message) - return super(crm_lead,self).write(cr, uid, ids, vals, 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 partner is now set to %s." % (self.case_get_note_msg_prefix(cr, uid, lead, context=context), lead.partner_id.name)) + lead.message_append_note(body=message) + return True + + def convert_opportunity_send_note(self, cr, uid, lead, context=None): + message = _("Lead has been converted to an opportunity.") + lead.message_append_note(body=message) + return True crm_lead()