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