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