[MERGE]: Merged with lp:openobject-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
315     def _check(self, cr, uid, ids=False, context=None):
316         """ Override of the base.stage method.
317             Function called by the scheduler to process cases for date actions
318             Only works on not done and cancelled cases
319         """
320         cr.execute('select * from crm_case \
321                 where (date_action_last<%s or date_action_last is null) \
322                 and (date_action_next<=%s or date_action_next is null) \
323                 and state not in (\'cancel\',\'done\')',
324                 (time.strftime("%Y-%m-%d %H:%M:%S"),
325                     time.strftime('%Y-%m-%d %H:%M:%S')))
326
327         ids2 = map(lambda x: x[0], cr.fetchall() or [])
328         cases = self.browse(cr, uid, ids2, context=context)
329         return self._action(cr, uid, cases, False, context=context)
330
331     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
332         """ Override of the base.stage method
333             Parameter of the stage search taken from the lead:
334             - type: stage type must be the same or 'both'
335             - section_id: if set, stages must belong to this section or
336               be a default stage; if not set, stages must be default
337               stages
338         """
339         if isinstance(cases, (int, long)):
340             cases = self.browse(cr, uid, cases, context=context)
341         # collect all section_ids
342         section_ids = []
343         types = ['both']
344         if section_id:
345             section_ids.append(section_id)
346         for lead in cases:
347             if lead.section_id:
348                 section_ids.append(lead.section_id.id)
349             if lead.type not in types:
350                 types.append(lead.type)
351         # OR all section_ids and OR with case_default
352         search_domain = []
353         if section_ids:
354             search_domain += [('|')] * len(section_ids)
355             for section_id in section_ids:
356                 search_domain.append(('section_ids', '=', section_id))
357         search_domain.append(('case_default', '=', True))
358         # AND with cases types
359         search_domain.append(('type', 'in', types))
360         # AND with the domain in parameter
361         search_domain += list(domain)
362         # perform search, return the first found
363         stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
364         if stage_ids:
365             return stage_ids[0]
366         return False
367
368     def case_cancel(self, cr, uid, ids, context=None):
369         """ Overrides case_cancel from base_stage to set probability """
370         res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
371         self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
372         return res
373
374     def case_reset(self, cr, uid, ids, context=None):
375         """ Overrides case_reset from base_stage to set probability """
376         res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
377         self.write(cr, uid, ids, {'probability': 0.0}, context=context)
378         return res
379
380     def case_mark_lost(self, cr, uid, ids, context=None):
381         """ Mark the case as lost: state=cancel and probability=0 """
382         for lead in self.browse(cr, uid, ids):
383             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
384             if stage_id:
385                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
386         self.case_mark_lost_send_note(cr, uid, ids, context=context)
387         return True
388
389     def case_mark_won(self, cr, uid, ids, context=None):
390         """ Mark the case as lost: state=done and probability=100 """
391         for lead in self.browse(cr, uid, ids):
392             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
393             if stage_id:
394                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
395         self.case_mark_won_send_note(cr, uid, ids, context=context)
396         return True
397
398     def set_priority(self, cr, uid, ids, priority):
399         """ Set lead priority
400         """
401         return self.write(cr, uid, ids, {'priority' : priority})
402
403     def set_high_priority(self, cr, uid, ids, context=None):
404         """ Set lead priority to high
405         """
406         return self.set_priority(cr, uid, ids, '1')
407
408     def set_normal_priority(self, cr, uid, ids, context=None):
409         """ Set lead priority to normal
410         """
411         return self.set_priority(cr, uid, ids, '3')
412
413     def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
414         # prepare opportunity data into dictionary for merging
415         opportunities = self.browse(cr, uid, ids, context=context)
416         def _get_first_not_null(attr):
417             if hasattr(oldest, attr):
418                 return getattr(oldest, attr)
419             for opportunity in opportunities:
420                 if hasattr(opportunity, attr):
421                     return getattr(opportunity, attr)
422             return False
423
424         def _get_first_not_null_id(attr):
425             res = _get_first_not_null(attr)
426             return res and res.id or False
427
428         def _concat_all(attr):
429             return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
430
431         data = {}
432         for field_name in fields:
433             field_info = self._all_columns.get(field_name)
434             if field_info is None:
435                 continue
436             field = field_info.column
437             if field._type in ('many2many', 'one2many'):
438                 continue
439             elif field._type == 'many2one':
440                 data[field_name] = _get_first_not_null_id(field_name)  # !!
441             elif field._type == 'text':
442                 data[field_name] = _concat_all(field_name)  #not lost
443             else:
444                 data[field_name] = _get_first_not_null(field_name)  #not lost
445         return data
446
447     def _merge_find_oldest(self, cr, uid, ids, context=None):
448         if context is None:
449             context = {}
450         #TOCHECK: where pass 'convert' in context ?
451         if context.get('convert'):
452             ids = list(set(ids) - set(context.get('lead_ids', False)) )
453
454         #search opportunities order by create date
455         opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
456         oldest_id = opportunity_ids[0]
457         return self.browse(cr, uid, oldest_id, context=context)
458
459     def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
460         body = []
461         if title:
462             body.append("%s\n" % (title))
463         for field_name in fields:
464             field_info = self._all_columns.get(field_name)
465             if field_info is None:
466                 continue
467             field = field_info.column
468             value = None
469
470             if field._type == 'selection':
471                 if hasattr(field.selection, '__call__'):
472                     key = field.selection(self, cr, uid, context=context)
473                 else:
474                     key = field.selection
475                 value = dict(key).get(lead[field_name], lead[field_name])
476             elif field._type == 'many2one':
477                 if lead[field_name]:
478                     value = lead[field_name].name_get()[0][1]
479             else:
480                 value = lead[field_name]
481
482             body.append("%s: %s" % (field.string, value or ''))
483         return "\n".join(body + ['---'])
484
485     def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
486         #TOFIX: mail template should be used instead of fix body, subject text
487         details = []
488         merge_message = _('Merged opportunities')
489         subject = [merge_message]
490         fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_ids', 'channel_id', 'company_id', 'contact_name',
491                   'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
492                   'country_id', 'city', 'street', 'street2', 'zip']
493         for opportunity in opportunities:
494             subject.append(opportunity.name)
495             title = "%s : %s" % (merge_message, opportunity.name)
496             details.append(self._mail_body(cr, uid, opportunity, fields, title=title, context=context))
497
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 data into first opportunity
568         self.write(cr, uid, [first_opportunity.id], data, context=context)
569
570         #copy message and attachements into the first opportunity
571         self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
572         self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
573
574         #Notification about loss of information
575         self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
576         #delete tail opportunities
577         self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
578
579         #open first opportunity
580         self.case_open(cr, uid, [first_opportunity.id])
581         return first_opportunity.id
582
583     def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
584         crm_stage = self.pool.get('crm.case.stage')
585         contact_id = False
586         if customer:
587             contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
588         if not section_id:
589             section_id = lead.section_id and lead.section_id.id or False
590         if section_id:
591             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
592         else:
593             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
594         stage_id = stage_ids and stage_ids[0] or False
595         return {
596                 'planned_revenue': lead.planned_revenue,
597                 'probability': lead.probability,
598                 'name': lead.name,
599                 'partner_id': customer and customer.id or False,
600                 'user_id': (lead.user_id and lead.user_id.id),
601                 'type': 'opportunity',
602                 'stage_id': stage_id or False,
603                 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
604                 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
605                 'email_from': customer and customer.email or lead.email_from,
606                 'phone': customer and customer.phone or lead.phone,
607         }
608
609     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
610         partner = self.pool.get('res.partner')
611         mail_message = self.pool.get('mail.message')
612         customer = False
613         if partner_id:
614             customer = partner.browse(cr, uid, partner_id, context=context)
615         for lead in self.browse(cr, uid, ids, context=context):
616             if lead.state in ('done', 'cancel'):
617                 continue
618             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
619             self.write(cr, uid, [lead.id], vals, context=context)
620             self.convert_opportunity_send_note(cr, uid, lead, context=context)
621
622         if user_ids or section_id:
623             self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
624
625         return True
626
627     def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
628         partner = self.pool.get('res.partner')
629         vals = { 'name': name,
630             'user_id': lead.user_id.id,
631             'comment': lead.description,
632             'section_id': lead.section_id.id or False,
633             'parent_id': parent_id,
634             'phone': lead.phone,
635             'mobile': lead.mobile,
636             'email': lead.email_from and tools.email_split(lead.email_from)[0],
637             'fax': lead.fax,
638             'title': lead.title and lead.title.id or False,
639             'function': lead.function,
640             'street': lead.street,
641             'street2': lead.street2,
642             'zip': lead.zip,
643             'city': lead.city,
644             'country_id': lead.country_id and lead.country_id.id or False,
645             'state_id': lead.state_id and lead.state_id.id or False,
646             'is_company': is_company,
647             'type': 'contact'
648         }
649         partner = partner.create(cr, uid,vals, context)
650         return partner
651
652     def _create_lead_partner(self, cr, uid, lead, context=None):
653         partner_id =  False
654         if lead.partner_name and lead.contact_name:
655             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
656             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
657         elif lead.partner_name and not lead.contact_name:
658             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
659         elif not lead.partner_name and lead.contact_name:
660             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
661         else:
662             partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
663         return partner_id
664
665     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
666         res = False
667         res_partner = self.pool.get('res.partner')
668         if partner_id:
669             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
670             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
671             res = lead.write({'partner_id' : partner_id, }, context=context)
672             self._lead_set_partner_send_note(cr, uid, [lead.id], context)
673         return res
674
675     def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
676         """
677         This function convert partner based on action.
678         if action is 'create', create new partner with contact and assign lead to new partner_id.
679         otherwise assign lead to specified partner_id
680         """
681         if context is None:
682             context = {}
683         partner_ids = {}
684         force_partner_id = partner_id
685         for lead in self.browse(cr, uid, ids, context=context):
686             if action == 'create':
687                 if not partner_id:
688                     partner_id = self._create_lead_partner(cr, uid, lead, context)
689                 partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context=context)
690             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
691             partner_ids[lead.id] = partner_id
692         return partner_ids
693
694     def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
695         index = 0
696         for lead_id in ids:
697             value = {}
698             if team_id:
699                 value['section_id'] = team_id
700             if index < len(user_ids):
701                 value['user_id'] = user_ids[index]
702                 index += 1
703             if value:
704                 self.write(cr, uid, [lead_id], value, context=context)
705         return True
706
707     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):
708         """
709         action :('schedule','Schedule a call'), ('log','Log a call')
710         """
711         phonecall = self.pool.get('crm.phonecall')
712         model_data = self.pool.get('ir.model.data')
713         phonecall_dict = {}
714         if not categ_id:
715             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
716             if res_id:
717                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
718         for lead in self.browse(cr, uid, ids, context=context):
719             if not section_id:
720                 section_id = lead.section_id and lead.section_id.id or False
721             if not user_id:
722                 user_id = lead.user_id and lead.user_id.id or False
723             vals = {
724                     'name' : call_summary,
725                     'opportunity_id' : lead.id,
726                     'user_id' : user_id or False,
727                     'categ_id' : categ_id or False,
728                     'description' : desc or '',
729                     'date' : schedule_time,
730                     'section_id' : section_id or False,
731                     'partner_id': lead.partner_id and lead.partner_id.id or False,
732                     'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
733                     'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
734                     'priority': lead.priority,
735             }
736             new_id = phonecall.create(cr, uid, vals, context=context)
737             phonecall.case_open(cr, uid, [new_id], context=context)
738             if action == 'log':
739                 phonecall.case_close(cr, uid, [new_id], context=context)
740             phonecall_dict[lead.id] = new_id
741             self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
742         return phonecall_dict
743
744
745     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
746         models_data = self.pool.get('ir.model.data')
747
748         # Get Opportunity views
749         form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
750         tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
751         return {
752                 'name': _('Opportunity'),
753                 'view_type': 'form',
754                 'view_mode': 'tree, form',
755                 'res_model': 'crm.lead',
756                 'domain': [('type', '=', 'opportunity')],
757                 'res_id': int(opportunity_id),
758                 'view_id': False,
759                 'views': [(form_view and form_view[1] or False, 'form'),
760                           (tree_view and tree_view[1] or False, 'tree'),
761                           (False, 'calendar'), (False, 'graph')],
762                 'type': 'ir.actions.act_window',
763         }
764
765     def action_makeMeeting(self, cr, uid, ids, context=None):
766         """ This opens Meeting's calendar view to schedule meeting on current Opportunity
767             @return : Dictionary value for created Meeting view
768         """
769         opportunity = self.browse(cr, uid, ids[0], context)
770         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
771         res['context'] = {
772             'default_opportunity_id': opportunity.id,
773             'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
774             'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
775             'default_user_id': uid,
776             'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
777             'default_email_from': opportunity.email_from,
778             'default_state': 'open',
779             'default_name': opportunity.name,
780         }
781         return res
782
783     def unlink(self, cr, uid, ids, context=None):
784         for lead in self.browse(cr, uid, ids, context):
785             if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
786                 raise osv.except_osv(_('Error!'),
787                     _("You cannot delete lead '%s' because it is not in 'Draft' state. " \
788                       "You can still cancel it, instead of deleting it.") % lead.name)
789         return super(crm_lead, self).unlink(cr, uid, ids, context)
790
791     def write(self, cr, uid, ids, vals, context=None):
792         if vals.get('stage_id') and not vals.get('probability'):
793             # change probability of lead(s) if required by stage
794             stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
795             if stage.on_change:
796                 vals['probability'] = stage.probability
797         if vals.get('section_id'):
798             section_id = self.pool.get('crm.case.section').browse(cr, uid, vals.get('section_id'), context=context)
799             if section_id:
800                 vals['message_follower_ids'] = [(4, follower.id) for follower in section_id.message_follower_ids]
801         return super(crm_lead,self).write(cr, uid, ids, vals, context) 
802
803     # ----------------------------------------
804     # Mail Gateway
805     # ----------------------------------------
806
807     def message_new(self, cr, uid, msg, custom_values=None, context=None):
808         """ Overrides mail_thread message_new that is called by the mailgateway
809             through message_process.
810             This override updates the document according to the email.
811         """
812         if custom_values is None: custom_values = {}
813         custom_values.update({
814             'name':  msg.get('subject') or _("No Subject"),
815             'description': msg.get('body'),
816             'email_from': msg.get('from'),
817             'email_cc': msg.get('cc'),
818             'user_id': False,
819         })
820         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
821             custom_values['priority'] = msg.get('priority')
822         return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
823
824     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
825         """ Overrides mail_thread message_update that is called by the mailgateway
826             through message_process.
827             This method updates the document according to the email.
828         """
829         if isinstance(ids, (str, int, long)):
830             ids = [ids]
831         if update_vals is None: update_vals = {}
832
833         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
834             update_vals['priority'] = msg.get('priority')
835         maps = {
836             'cost':'planned_cost',
837             'revenue': 'planned_revenue',
838             'probability':'probability',
839         }
840         for line in msg.get('body', '').split('\n'):
841             line = line.strip()
842             res = tools.misc.command_re.match(line)
843             if res and maps.get(res.group(1).lower()):
844                 key = maps.get(res.group(1).lower())
845                 update_vals[key] = res.group(2).lower()
846
847         return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
848
849     # ----------------------------------------
850     # OpenChatter methods and notifications
851     # ----------------------------------------
852
853     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
854         """ Override of the (void) default notification method. """
855         stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
856         return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
857
858     def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
859         if isinstance(lead, (int, long)):
860             lead = self.browse(cr, uid, [lead], context=context)[0]
861         return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
862
863     def create_send_note(self, cr, uid, ids, context=None):
864         for id in ids:
865             message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
866             self.message_post(cr, uid, [id], body=message, context=context)
867         return True
868
869     def case_mark_lost_send_note(self, cr, uid, ids, context=None):
870         message = _("Opportunity has been <b>lost</b>.")
871         return self.message_post(cr, uid, ids, body=message, context=context)
872
873     def case_mark_won_send_note(self, cr, uid, ids, context=None):
874         message = _("Opportunity has been <b>won</b>.")
875         return self.message_post(cr, uid, ids, body=message, context=context)
876
877     def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
878         phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
879         if action == 'log': prefix = 'Logged'
880         else: prefix = 'Scheduled'
881         message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
882         return self.message_post(cr, uid, ids, body=message, context=context)
883
884     def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
885         for lead in self.browse(cr, uid, ids, context=context):
886             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))
887             lead.message_post(body=message)
888         return True
889
890     def convert_opportunity_send_note(self, cr, uid, lead, context=None):
891         message = _("Lead has been <b>converted to an opportunity</b>.")
892         lead.message_post(body=message)
893         return True
894
895 crm_lead()
896
897 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: