[IMP] crm_lead yml tests.
[odoo/odoo.git] / addons / crm / crm_lead.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import fields, osv
23 from datetime import datetime
24 import crm
25 import time
26 from tools.translate import _
27 from crm import crm_case
28 import binascii
29 import tools
30 from mail.mail_message import to_email
31
32 CRM_LEAD_PENDING_STATES = (
33     crm.AVAILABLE_STATES[2][0], # Cancelled
34     crm.AVAILABLE_STATES[3][0], # Done
35     crm.AVAILABLE_STATES[4][0], # Pending
36 )
37
38 class crm_lead(crm_case, osv.osv):
39     """ CRM Lead Case """
40     _name = "crm.lead"
41     _description = "Lead/Opportunity"
42     _order = "priority,date_action,id desc"
43     _inherit = ['ir.needaction_mixin', 'mail.thread','res.partner']
44
45     def _get_default_section_id(self, cr, uid, context=None):
46         """ Gives default section by checking if present in the context """
47         return self._resolve_section_id_from_context(cr, uid, context=context)
48
49     def _resolve_section_id_from_context(self, cr, uid, context=None):
50         """ Returns ID of section based on the value of 'section_id'
51             context key, or None if it cannot be resolved to a single
52             Sales Team.
53         """
54         if context is None:
55             context = {}
56         if type(context.get('default_section_id')) in (int, long):
57             return context.get('default_section_id')
58         if isinstance(context.get('default_section_id'), basestring):
59             section_name = context['default_section_id']
60             section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
61             if len(section_ids) == 1:
62                 return int(section_ids[0][0])
63         return False
64
65     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
66         access_rights_uid = access_rights_uid or uid
67         stage_obj = self.pool.get('crm.case.stage')
68         order = stage_obj._order
69         # lame hack to allow reverting search, should just work in the trivial case
70         if read_group_order == 'stage_id desc':
71             order = "%s desc" % order
72         # retrieve section_id from the context and write the domain
73         search_domain = []
74         section_id = self._resolve_section_id_from_context(cr, uid, context=context)
75         if section_id:
76             search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', True)]
77         search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', 1), ('fold', '=', False)]
78         # perform search
79         stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
80         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
81         # restore order of the search
82         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
83         return result
84
85     _group_by_full = {
86         'stage_id': _read_group_stage_ids
87     }
88
89     def _compute_day(self, cr, uid, ids, fields, args, context=None):
90         """
91         @param cr: the current row, from the database cursor,
92         @param uid: the current user’s ID for security checks,
93         @param ids: List of Openday’s IDs
94         @return: difference between current date and log date
95         @param context: A standard dictionary for contextual values
96         """
97         cal_obj = self.pool.get('resource.calendar')
98         res_obj = self.pool.get('resource.resource')
99
100         res = {}
101         for lead in self.browse(cr, uid, ids, context=context):
102             for field in fields:
103                 res[lead.id] = {}
104                 duration = 0
105                 ans = False
106                 if field == 'day_open':
107                     if lead.date_open:
108                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
109                         date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
110                         ans = date_open - date_create
111                         date_until = lead.date_open
112                 elif field == 'day_close':
113                     if lead.date_closed:
114                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
115                         date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
116                         date_until = lead.date_closed
117                         ans = date_close - date_create
118                 if ans:
119                     resource_id = False
120                     if lead.user_id:
121                         resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
122                         if len(resource_ids):
123                             resource_id = resource_ids[0]
124
125                     duration = float(ans.days)
126                     if lead.section_id and lead.section_id.resource_calendar_id:
127                         duration =  float(ans.days) * 24
128                         new_dates = cal_obj.interval_get(cr,
129                             uid,
130                             lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
131                             datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
132                             duration,
133                             resource=resource_id
134                         )
135                         no_days = []
136                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
137                         for in_time, out_time in new_dates:
138                             if in_time.date not in no_days:
139                                 no_days.append(in_time.date)
140                             if out_time > date_until:
141                                 break
142                         duration =  len(no_days)
143                 res[lead.id][field] = abs(int(duration))
144         return res
145
146     def _history_search(self, cr, uid, obj, name, args, context=None):
147         res = []
148         msg_obj = self.pool.get('mail.message')
149         message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
150         lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
151
152         if lead_ids:
153             return [('id', 'in', lead_ids)]
154         else:
155             return [('id', '=', '0')]
156
157     def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
158         res = {}
159         for obj in self.browse(cr, uid, ids, context=context):
160             res[obj.id] = ''
161             for msg in obj.message_ids:
162                 if msg.email_from:
163                     res[obj.id] = msg.subject
164                     break
165         return res
166
167     _columns = {
168         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
169             select=True, help="Optional linked partner, usually after conversion of the lead"),
170
171         'id': fields.integer('ID', readonly=True),
172         'name': fields.char('Name', size=64, select=1),
173         'active': fields.boolean('Active', required=False),
174         'date_action_last': fields.datetime('Last Action', readonly=1),
175         'date_action_next': fields.datetime('Next Action', readonly=1),
176         'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
177         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
178                         select=True, help='When sending mails, the default email address is taken from the sales team.'),
179         'create_date': fields.datetime('Creation Date' , readonly=True),
180         '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"),
181         'description': fields.text('Notes'),
182         'write_date': fields.datetime('Update Date' , readonly=True),
183         'categ_id': fields.many2one('crm.case.categ', 'Category', \
184             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
185         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
186             domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
187         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
188         'contact_name': fields.char('Contact Name', size=64),
189         '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),
190         'opt_in': fields.boolean('Opt-In', oldname='optin', help="If opt-in is checked, this contact has accepted to receive emails."),
191         '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."),
192         'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
193         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
194         'date_closed': fields.datetime('Closed', readonly=True),
195         'stage_id': fields.many2one('crm.case.stage', 'Stage',
196                         domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
197         'user_id': fields.many2one('res.users', 'Salesperson'),
198         'referred': fields.char('Referred By', size=64),
199         'date_open': fields.datetime('Opened', readonly=True),
200         'day_open': fields.function(_compute_day, string='Days to Open', \
201                                 multi='day_open', type="float", store=True),
202         'day_close': fields.function(_compute_day, string='Days to Close', \
203                                 multi='day_close', type="float", store=True),
204         'state': fields.related('stage_id', 'state', type="selection", store=True,
205                 selection=crm.AVAILABLE_STATES, string="State", readonly=True,
206                 help='The state is set to \'Draft\', when a case is created.\
207                       If the case is in progress the state is set to \'Open\'.\
208                       When the case is over, the state is set to \'Done\'.\
209                       If the case needs to be reviewed then the state is \
210                       set to \'Pending\'.'),
211         'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
212         'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
213
214         # Only used for type opportunity
215         'probability': fields.float('Success Rate (%)',group_operator="avg"),
216         'planned_revenue': fields.float('Expected Revenue'),
217         'ref': fields.reference('Reference', selection=crm._links_get, size=128),
218         'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
219         'phone': fields.char("Phone", size=64),
220         'date_deadline': fields.date('Expected Closing'),
221         'date_action': fields.date('Next Action Date', select=True),
222         'title_action': fields.char('Next Action', size=64),
223         'color': fields.integer('Color Index'),
224         'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
225         'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
226         'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
227         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
228         'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
229
230     }
231
232     _defaults = {
233         'active': 1,
234         'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
235         'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
236         'type': 'lead',
237         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
238         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
239         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
240         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
241         'color': 0,
242     }
243
244     def get_needaction_user_ids(self, cr, uid, ids, context=None):
245         result = dict.fromkeys(ids, [])
246         for obj in self.browse(cr, uid, ids, context=context):
247             # salesman must perform an action when in draft mode
248             if obj.state == 'draft' and obj.user_id:
249                 result[obj.id] = [obj.user_id.id]
250         return result
251     
252     def create(self, cr, uid, vals, context=None):
253         obj_id = super(crm_lead, self).create(cr, uid, vals, context)
254         self.create_send_note(cr, uid, [obj_id], context=context)
255         return obj_id
256     
257     def on_change_opt_in(self, cr, uid, ids, opt_in):
258         return {'value':{'opt_in':opt_in,'opt_out':False}}
259
260     def on_change_opt_out(self, cr, uid, ids, opt_out):
261         return {'value':{'opt_out':opt_out,'opt_in':False}}
262
263     def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
264         if not stage_id:
265             return {'value':{}}
266         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
267         if not stage.on_change:
268             return {'value':{}}
269         return {'value':{'probability': stage.probability}}
270
271     def _get_default_stage_id(self, cr, uid, context=None):
272         """ Gives default stage_id """
273         section_id = self._get_default_section_id(cr, uid, context=context)
274         return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'), ('type', '=', 'both')], context=context)
275
276     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
277         """ Override of the base.stage method
278             Parameter of the stage search taken from the lead:
279             - type: stage type must be the same or 'both'
280             - section_id: if set, stages must belong to this section or
281               be a default case
282         """
283         if isinstance(cases, (int, long)):
284             cases = self.browse(cr, uid, cases, context=context)
285         domain = list(domain)
286         if section_id:
287                 domain += ['|', ('section_ids', '=', section_id), ('case_default', '=', True)]
288         for lead in cases:
289             lead_section_id = lead.section_id.id if lead.section_id else None
290             domain += ['|', ('type', '=', lead.type), ('type', '=', 'both')]
291             if lead_section_id:
292                 domain += ['|', ('section_ids', '=', lead_section_id), ('case_default', '=', True)]
293         stage_ids = self.pool.get('crm.case.stage').search(cr, uid, domain, order=order, context=context)
294         if stage_ids:
295             return stage_ids[0]
296         return False
297
298     def case_cancel(self, cr, uid, ids, context=None):
299         """Overrides cancel for crm_case for setting probability
300         """
301         res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
302         self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
303         return res
304
305     def case_reset(self, cr, uid, ids, context=None):
306         """Overrides reset as draft in order to set the stage field as empty
307         """
308         res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
309         self.write(cr, uid, ids, {'probability': 0.0}, context=context)
310         return res
311
312     def case_mark_lost(self, cr, uid, ids, context=None):
313         """Mark the case as lost: state = done and probability = 0%
314         """
315         for lead in self.browse(cr, uid, ids):
316             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
317             if stage_id:
318                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
319         self.case_mark_lost_send_note(cr, uid, ids, context=context)
320         return True
321
322     def case_mark_won(self, cr, uid, ids, context=None):
323         """Mark the case as lost: state = done and probability = 0%
324         """
325         for lead in self.browse(cr, uid, ids):
326             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
327             if stage_id:
328                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
329         self.case_mark_won_send_note(cr, uid, ids, context=context)
330         return True
331
332     def set_priority(self, cr, uid, ids, priority):
333         """Set lead priority
334         """
335         return self.write(cr, uid, ids, {'priority' : priority})
336
337     def set_high_priority(self, cr, uid, ids, context=None):
338         """Set lead priority to high
339         """
340         return self.set_priority(cr, uid, ids, '1')
341
342     def set_normal_priority(self, cr, uid, ids, context=None):
343         """Set lead priority to normal
344         """
345         return self.set_priority(cr, uid, ids, '3')
346
347
348     def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
349         # prepare opportunity data into dictionary for merging
350         opportunities = self.browse(cr, uid, ids, context=context)
351         def _get_first_not_null(attr):
352             if hasattr(oldest, attr):
353                 return getattr(oldest, attr)
354             for opportunity in opportunities:
355                 if hasattr(opportunity, attr):
356                     return getattr(opportunity, attr)
357             return False
358
359         def _get_first_not_null_id(attr):
360             res = _get_first_not_null(attr)
361             return res and res.id or False
362
363         def _concat_all(attr):
364             return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
365
366         data = {}
367         for field_name in fields:
368             field_info = self._all_columns.get(field_name)
369             if field_info is None:
370                 continue
371             field = field_info.column
372             if field._type in ('many2many', 'one2many'):
373                 continue
374             elif field._type == 'many2one':
375                 data[field_name] = _get_first_not_null_id(field_name)  # !!
376             elif field._type == 'text':
377                 data[field_name] = _concat_all(field_name)  #not lost
378             else:
379                 data[field_name] = _get_first_not_null(field_name)  #not lost
380         return data
381
382     def _merge_find_oldest(self, cr, uid, ids, context=None):
383         if context is None:
384             context = {}
385         #TOCHECK: where pass 'convert' in context ?
386         if context.get('convert'):
387             ids = list(set(ids) - set(context.get('lead_ids', False)) )
388
389         #search opportunities order by create date
390         opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
391         oldest_id = opportunity_ids[0]
392         return self.browse(cr, uid, oldest_id, context=context)
393
394     def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
395         body = []
396         if title:
397             body.append("%s\n" % (title))
398         for field_name in fields:
399             field_info = self._all_columns.get(field_name)
400             if field_info is None:
401                 continue
402             field = field_info.column
403             value = None
404
405             if field._type == 'selection':
406                 if hasattr(field.selection, '__call__'):
407                     key = field.selection(self, cr, uid, context=context)
408                 else:
409                     key = field.selection
410                 value = dict(key).get(lead[field_name], lead[field_name])
411             elif field._type == 'many2one':
412                 if lead[field_name]:
413                     value = lead[field_name].name_get()[0][1]
414             else:
415                 value = lead[field_name]
416
417             body.append("%s: %s" % (field.string, value or ''))
418         return "\n".join(body + ['---'])
419
420     def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
421         #TOFIX: mail template should be used instead of fix body, subject text
422         details = []
423         merge_message = _('Merged opportunities')
424         subject = [merge_message]
425         fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
426                   'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
427                   'country_id', 'city', 'street', 'street2', 'zip']
428         for opportunity in opportunities:
429             subject.append(opportunity.name)
430             title = "%s : %s" % (merge_message, opportunity.name)
431             details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
432
433         subject = subject[0] + ", ".join(subject[1:])
434         details = "\n\n".join(details)
435         return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
436
437     def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
438         message = self.pool.get('mail.message')
439         for opportunity in opportunities:
440             for history in opportunity.message_ids:
441                 message.write(cr, uid, history.id, {
442                         'res_id': opportunity_id,
443                         'subject' : _("From %s : %s") % (opportunity.name, history.subject)
444                 }, context=context)
445
446         return True
447
448     def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
449         attachment = self.pool.get('ir.attachment')
450
451         # return attachments of opportunity
452         def _get_attachments(opportunity_id):
453             attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
454             return attachment.browse(cr, uid, attachment_ids, context=context)
455
456         count = 1
457         first_attachments = _get_attachments(opportunity_id)
458         for opportunity in opportunities:
459             attachments = _get_attachments(opportunity.id)
460             for first in first_attachments:
461                 for attachment in attachments:
462                     if attachment.name == first.name:
463                         values = dict(
464                             name = "%s (%s)" % (attachment.name, count,),
465                             res_id = opportunity_id,
466                         )
467                         attachment.write(values)
468                         count+=1
469
470         return True
471
472     def merge_opportunity(self, cr, uid, ids, context=None):
473         """
474         To merge opportunities
475             :param ids: list of opportunities ids to merge
476         """
477         if context is None: context = {}
478
479         #TOCHECK: where pass lead_ids in context?
480         lead_ids = context and context.get('lead_ids', []) or []
481
482         if len(ids) <= 1:
483             raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
484
485         ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
486         opportunities = self.browse(cr, uid, ids, context=context)
487         opportunities_list = list(set(opportunities) - set(ctx_opportunities))
488         oldest = self._merge_find_oldest(cr, uid, ids, context=context)
489         if ctx_opportunities :
490             first_opportunity = ctx_opportunities[0]
491             tail_opportunities = opportunities_list
492         else:
493             first_opportunity = opportunities_list[0]
494             tail_opportunities = opportunities_list[1:]
495
496         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',
497             'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
498             'date_action_next', 'email_from', 'email_cc', 'partner_name']
499
500         data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
501
502         # merge data into first opportunity
503         self.write(cr, uid, [first_opportunity.id], data, context=context)
504
505         #copy message and attachements into the first opportunity
506         self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
507         self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
508
509         #Notification about loss of information
510         self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
511         #delete tail opportunities
512         self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
513
514         #open first opportunity
515         self.case_open(cr, uid, [first_opportunity.id])
516         return first_opportunity.id
517
518     def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
519         crm_stage = self.pool.get('crm.case.stage')
520         contact_id = False
521         if customer:
522             contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
523         if not section_id:
524             section_id = lead.section_id and lead.section_id.id or False
525         if section_id:
526             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
527         else:
528             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
529         stage_id = stage_ids and stage_ids[0] or False
530         return {
531                 'planned_revenue': lead.planned_revenue,
532                 'probability': lead.probability,
533                 'name': lead.name,
534                 'partner_id': customer and customer.id or False,
535                 'user_id': (lead.user_id and lead.user_id.id),
536                 'type': 'opportunity',
537                 'stage_id': stage_id or False,
538                 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
539                 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
540         }
541
542     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
543         partner = self.pool.get('res.partner')
544         mail_message = self.pool.get('mail.message')
545         customer = False
546         if partner_id:
547             customer = partner.browse(cr, uid, partner_id, context=context)
548         for lead in self.browse(cr, uid, ids, context=context):
549             if lead.state in ('done', 'cancel'):
550                 continue
551             if user_ids or section_id:
552                 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
553
554             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
555             self.write(cr, uid, [lead.id], vals, context=context)
556
557             self.convert_opportunity_send_note(cr, uid, lead, context=context)
558             #TOCHECK: why need to change partner details in all messages of lead ?
559             if lead.partner_id:
560                 msg_ids = [ x.id for x in lead.message_ids]
561                 mail_message.write(cr, uid, msg_ids, {
562                         'partner_id': lead.partner_id.id
563                     }, context=context)
564         return True
565
566     def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
567         partner = self.pool.get('res.partner')
568         vals = { 'name': name,
569             'user_id': lead.user_id.id,
570             'comment': lead.description,
571             'section_id': lead.section_id.id or False,
572             'parent_id': parent_id,
573             'phone': lead.phone,
574             'mobile': lead.mobile,
575             'email': lead.email_from and to_email(lead.email_from)[0],
576             'fax': lead.fax,
577             'title': lead.title and lead.title.id or False,
578             'function': lead.function,
579             'street': lead.street,
580             'street2': lead.street2,
581             'zip': lead.zip,
582             'city': lead.city,
583             'country_id': lead.country_id and lead.country_id.id or False,
584             'state_id': lead.state_id and lead.state_id.id or False,
585             'is_company': is_company,
586             'type': 'contact'
587         }
588         partner = partner.create(cr, uid,vals, context)
589         return partner
590
591     def _create_lead_partner(self, cr, uid, lead, context=None):
592         partner_id =  False
593         if lead.partner_name and lead.contact_name:
594             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
595             self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
596         elif lead.partner_name and not lead.contact_name:
597             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
598         elif not lead.partner_name and lead.contact_name:
599             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
600         else:
601             partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
602         return partner_id
603
604     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
605         res = False
606         res_partner = self.pool.get('res.partner')
607         if partner_id:
608             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
609             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
610             res = lead.write({'partner_id' : partner_id, }, context=context)
611             self._lead_set_partner_send_note(cr, uid, [lead.id], context)
612         return res
613
614     def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
615         """
616         This function convert partner based on action.
617         if action is 'create', create new partner with contact and assign lead to new partner_id.
618         otherwise assign lead to specified partner_id
619         """
620         if context is None:
621             context = {}
622         partner_ids = {}
623         for lead in self.browse(cr, uid, ids, context=context):
624             if action == 'create':
625                 if not partner_id:
626                     partner_id = self._create_lead_partner(cr, uid, lead, context)
627             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
628             partner_ids[lead.id] = partner_id
629         return partner_ids
630
631     def _send_mail_to_salesman(self, cr, uid, lead, context=None):
632         """
633         Send mail to salesman with updated Lead details.
634         @ lead: browse record of 'crm.lead' object.
635         """
636         #TOFIX: mail template should be used here instead of fix subject, body text.
637         message = self.pool.get('mail.message')
638         email_to = lead.user_id and lead.user_id.user_email
639         if not email_to:
640             return False
641
642         email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
643         partner = lead.partner_id and lead.partner_id.name or lead.partner_name
644         subject = "lead %s converted into opportunity" % lead.name
645         body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
646         return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
647
648
649     def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
650         index = 0
651         for lead_id in ids:
652             value = {}
653             if team_id:
654                 value['section_id'] = team_id
655             if index < len(user_ids):
656                 value['user_id'] = user_ids[index]
657                 index += 1
658             if value:
659                 self.write(cr, uid, [lead_id], value, context=context)
660         return True
661
662     def schedule_phonecall(self, cr, uid, ids, schedule_time, call_summary, desc, phone, contact_name, user_id=False, section_id=False, categ_id=False, action='schedule', context=None):
663         """
664         action :('schedule','Schedule a call'), ('log','Log a call')
665         """
666         phonecall = self.pool.get('crm.phonecall')
667         model_data = self.pool.get('ir.model.data')
668         phonecall_dict = {}
669         if not categ_id:
670             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
671             if res_id:
672                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
673         for lead in self.browse(cr, uid, ids, context=context):
674             if not section_id:
675                 section_id = lead.section_id and lead.section_id.id or False
676             if not user_id:
677                 user_id = lead.user_id and lead.user_id.id or False
678             vals = {
679                     'name' : call_summary,
680                     'opportunity_id' : lead.id,
681                     'user_id' : user_id or False,
682                     'categ_id' : categ_id or False,
683                     'description' : desc or '',
684                     'date' : schedule_time,
685                     'section_id' : section_id or False,
686                     'partner_id': lead.partner_id and lead.partner_id.id or False,
687                     'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
688                     'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
689                     'priority': lead.priority,
690             }
691             new_id = phonecall.create(cr, uid, vals, context=context)
692             phonecall.case_open(cr, uid, [new_id], context=context)
693             if action == 'log':
694                 phonecall.case_close(cr, uid, [new_id], context=context)
695             phonecall_dict[lead.id] = new_id
696             self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
697         return phonecall_dict
698
699
700     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
701         models_data = self.pool.get('ir.model.data')
702
703         # Get Opportunity views
704         form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
705         tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
706         return {
707                 'name': _('Opportunity'),
708                 'view_type': 'form',
709                 'view_mode': 'tree, form',
710                 'res_model': 'crm.lead',
711                 'domain': [('type', '=', 'opportunity')],
712                 'res_id': int(opportunity_id),
713                 'view_id': False,
714                 'views': [(form_view and form_view[1] or False, 'form'),
715                           (tree_view and tree_view[1] or False, 'tree'),
716                           (False, 'calendar'), (False, 'graph')],
717                 'type': 'ir.actions.act_window',
718         }
719
720
721     def message_new(self, cr, uid, msg, custom_values=None, context=None):
722         """Automatically calls when new email message arrives"""
723         res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
724         subject = msg.get('subject')  or _("No Subject")
725         body = msg.get('body_text')
726
727         msg_from = msg.get('from')
728         priority = msg.get('priority')
729         vals = {
730             'name': subject,
731             'email_from': msg_from,
732             'email_cc': msg.get('cc'),
733             'description': body,
734             'user_id': False,
735         }
736         if priority:
737             vals['priority'] = priority
738         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
739         self.write(cr, uid, [res_id], vals, context)
740         return res_id
741
742     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
743         if isinstance(ids, (str, int, long)):
744             ids = [ids]
745         if vals == None:
746             vals = {}
747         super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
748
749         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
750             vals['priority'] = msg.get('priority')
751         maps = {
752             'cost':'planned_cost',
753             'revenue': 'planned_revenue',
754             'probability':'probability'
755         }
756         vls = {}
757         for line in msg['body_text'].split('\n'):
758             line = line.strip()
759             res = tools.misc.command_re.match(line)
760             if res and maps.get(res.group(1).lower()):
761                 key = maps.get(res.group(1).lower())
762                 vls[key] = res.group(2).lower()
763         vals.update(vls)
764
765         # Unfortunately the API is based on lists
766         # but we want to update the state based on the
767         # previous state, so we have to loop:
768         for case in self.browse(cr, uid, ids, context=context):
769             values = dict(vals)
770             if case.state in CRM_LEAD_PENDING_STATES:
771                 #re-open
772                 values.update(state=crm.AVAILABLE_STATES[1][0])
773                 if not case.date_open:
774                     values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
775             res = self.write(cr, uid, [case.id], values, context=context)
776         return res
777
778     def action_makeMeeting(self, cr, uid, ids, context=None):
779         """
780         This opens Meeting's calendar view to schedule meeting on current Opportunity
781         @return : Dictionary value for created Meeting view
782         """
783         if context is None:
784             context = {}
785         value = {}
786         data_obj = self.pool.get('ir.model.data')
787         for opp in self.browse(cr, uid, ids, context=context):
788             # Get meeting views
789             tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
790             form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
791             calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
792             search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
793             context.update({
794                 'default_opportunity_id': opp.id,
795                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
796                 'default_user_id': uid,
797                 'default_section_id': opp.section_id and opp.section_id.id or False,
798                 'default_email_from': opp.email_from,
799                 'default_state': 'open',
800                 'default_name': opp.name
801             })
802             value = {
803                 'name': _('Meetings'),
804                 'context': context,
805                 'view_type': 'form',
806                 'view_mode': 'calendar,form,tree',
807                 'res_model': 'crm.meeting',
808                 'view_id': False,
809                 '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')],
810                 'type': 'ir.actions.act_window',
811                 'search_view_id': search_view and search_view[1] or False,
812                 'nodestroy': True
813             }
814         return value
815
816
817     def unlink(self, cr, uid, ids, context=None):
818         for lead in self.browse(cr, uid, ids, context):
819             if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
820                 raise osv.except_osv(_('Error'),
821                     _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
822                       "You should better cancel it, instead of deleting it.") % lead.name)
823         return super(crm_lead, self).unlink(cr, uid, ids, context)
824
825
826     def write(self, cr, uid, ids, vals, context=None):
827         if not context:
828             context = {}
829
830         if 'date_closed' in vals:
831             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
832
833         if vals.get('stage_id'):
834             stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
835             # change probability of lead(s) if required by stage
836             if not vals.get('probability') and stage.on_change:
837                 vals['probability'] = stage.probability
838             for case in self.browse(cr, uid, ids, context=context):
839                 message = _("Stage changed to <b>%s</b>.") % (stage.name)
840                 case.message_append_note(body=message)
841         return super(crm_lead,self).write(cr, uid, ids, vals, context)
842     
843     # ----------------------------------------
844     # OpenChatter methods and notifications
845     # ----------------------------------------
846
847     def message_get_subscribers(self, cr, uid, ids, context=None):
848         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
849         # add salesman to the subscribers
850         for obj in self.browse(cr, uid, ids, context=context):
851             if obj.user_id:
852                 sub_ids.append(obj.user_id.id)
853         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
854     
855     def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
856         if isinstance(lead, (int, long)):
857             lead = self.browse(cr, uid, [lead], context=context)[0]
858         return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
859     
860     def create_send_note(self, cr, uid, ids, context=None):
861         for id in ids:
862             message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
863             self.message_append_note(cr, uid, [id], body=message, context=context)
864         return True
865
866     def case_mark_lost_send_note(self, cr, uid, ids, context=None):
867         message = _("Opportunity has been <b>lost</b>.")
868         return self.message_append_note(cr, uid, ids, body=message, context=context)
869
870     def case_mark_won_send_note(self, cr, uid, ids, context=None):
871         message = _("Opportunity has been <b>won</b>.")
872         return self.message_append_note(cr, uid, ids, body=message, context=context)
873
874     def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
875         phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
876         if action == 'log': prefix = 'Logged'
877         else: prefix = 'Scheduled'
878         message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
879         return self. message_append_note(cr, uid, ids, body=message, context=context)
880
881     def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
882         for lead in self.browse(cr, uid, ids, context=context):
883             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))
884             lead.message_append_note(body=message)
885         return True
886     
887     def convert_opportunity_send_note(self, cr, uid, lead, context=None):
888         message = _("Lead has been <b>converted to an opportunity</b>.")
889         lead.message_append_note(body=message)
890         return True
891
892 crm_lead()
893
894 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: