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