[MERGE] trunk-fix-mail-mail-rules-tde: OpenChatter 'Rule Yur Daddy' Branch
[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         # Chatter message's subject
499         subject = subject[0] + ": " + ", ".join(subject[1:])
500         details = "\n\n".join(details)
501         return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
502
503     def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
504         message = self.pool.get('mail.message')
505         for opportunity in opportunities:
506             for history in opportunity.message_ids:
507                 message.write(cr, uid, history.id, {
508                         'res_id': opportunity_id,
509                         'subject' : _("From %s : %s") % (opportunity.name, history.subject)
510                 }, context=context)
511
512         return True
513
514     def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
515         attachment = self.pool.get('ir.attachment')
516
517         # return attachments of opportunity
518         def _get_attachments(opportunity_id):
519             attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
520             return attachment.browse(cr, uid, attachment_ids, context=context)
521
522         count = 1
523         first_attachments = _get_attachments(opportunity_id)
524         for opportunity in opportunities:
525             attachments = _get_attachments(opportunity.id)
526             for first in first_attachments:
527                 for attachment in attachments:
528                     if attachment.name == first.name:
529                         values = dict(
530                             name = "%s (%s)" % (attachment.name, count,),
531                             res_id = opportunity_id,
532                         )
533                         attachment.write(values)
534                         count+=1
535
536         return True
537
538     def merge_opportunity(self, cr, uid, ids, context=None):
539         """
540         To merge opportunities
541             :param ids: list of opportunities ids to merge
542         """
543         if context is None: context = {}
544
545         #TOCHECK: where pass lead_ids in context?
546         lead_ids = context and context.get('lead_ids', []) or []
547
548         if len(ids) <= 1:
549             raise osv.except_osv(_('Warning!'),_('Please select more than one opportunity from the list view.'))
550
551         ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
552         opportunities = self.browse(cr, uid, ids, context=context)
553         opportunities_list = list(set(opportunities) - set(ctx_opportunities))
554         oldest = self._merge_find_oldest(cr, uid, ids, context=context)
555         if ctx_opportunities :
556             first_opportunity = ctx_opportunities[0]
557             tail_opportunities = opportunities_list + ctx_opportunities[1:]
558         else:
559             first_opportunity = opportunities_list[0]
560             tail_opportunities = opportunities_list[1:]
561
562         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',
563             'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
564             'date_action_next', 'email_from', 'email_cc', 'partner_name']
565
566         data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
567
568         # Merge messages and attachements into the first opportunity
569         self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
570         self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
571
572         # Merge notifications about loss of information
573         self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
574         # Write merged data into first opportunity
575         self.write(cr, uid, [first_opportunity.id], data, 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
589         if not section_id:
590             section_id = lead.section_id and lead.section_id.id or False
591
592         if section_id:
593             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
594         else:
595             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
596         stage_id = stage_ids and stage_ids[0] or False
597
598         return {
599                 'planned_revenue': lead.planned_revenue,
600                 'probability': lead.probability,
601                 'name': lead.name,
602                 'partner_id': customer and customer.id or False,
603                 'user_id': (lead.user_id and lead.user_id.id),
604                 'type': 'opportunity',
605                 'stage_id': stage_id or False,
606                 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
607                 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
608                 'email_from': customer and customer.email or lead.email_from,
609                 'phone': customer and customer.phone or lead.phone,
610         }
611
612     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
613         partner = self.pool.get('res.partner')
614         customer = False
615         if partner_id:
616             customer = partner.browse(cr, uid, partner_id, context=context)
617         for lead in self.browse(cr, uid, ids, context=context):
618             if lead.state in ('done', 'cancel'):
619                 continue
620             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
621             self.write(cr, uid, [lead.id], vals, context=context)
622             self.convert_opportunity_send_note(cr, uid, lead, context=context)
623
624         if user_ids or section_id:
625             self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
626
627         return True
628
629     def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
630         partner = self.pool.get('res.partner')
631         vals = { 'name': name,
632             'user_id': lead.user_id.id,
633             'comment': lead.description,
634             'section_id': lead.section_id.id or False,
635             'parent_id': parent_id,
636             'phone': lead.phone,
637             'mobile': lead.mobile,
638             'email': lead.email_from and tools.email_split(lead.email_from)[0],
639             'fax': lead.fax,
640             'title': lead.title and lead.title.id or False,
641             'function': lead.function,
642             'street': lead.street,
643             'street2': lead.street2,
644             'zip': lead.zip,
645             'city': lead.city,
646             'country_id': lead.country_id and lead.country_id.id or False,
647             'state_id': lead.state_id and lead.state_id.id or False,
648             'is_company': is_company,
649             'type': 'contact'
650         }
651         partner = partner.create(cr, uid,vals, context)
652         return partner
653
654     def _create_lead_partner(self, cr, uid, lead, context=None):
655         partner_id =  False
656         if lead.partner_name and lead.contact_name:
657             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
658             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
659         elif lead.partner_name and not lead.contact_name:
660             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
661         elif not lead.partner_name and lead.contact_name:
662             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
663         else:
664             partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
665         return partner_id
666
667     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
668         res = False
669         res_partner = self.pool.get('res.partner')
670         if partner_id:
671             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
672             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
673             res = lead.write({'partner_id' : partner_id, }, context=context)
674             self._lead_set_partner_send_note(cr, uid, [lead.id], context)
675         return res
676
677     def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
678         """
679         This function convert partner based on action.
680         if action is 'create', create new partner with contact and assign lead to new partner_id.
681         otherwise assign lead to specified partner_id
682         """
683         if context is None:
684             context = {}
685         partner_ids = {}
686         force_partner_id = partner_id
687         for lead in self.browse(cr, uid, ids, context=context):
688             if action == 'create':
689                 if not partner_id:
690                     partner_id = self._create_lead_partner(cr, uid, lead, context)
691                 partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context=context)
692             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
693             partner_ids[lead.id] = partner_id
694         return partner_ids
695
696     def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
697         index = 0
698         for lead_id in ids:
699             value = {}
700             if team_id:
701                 value['section_id'] = team_id
702             if index < len(user_ids):
703                 value['user_id'] = user_ids[index]
704                 index += 1
705             if value:
706                 self.write(cr, uid, [lead_id], value, context=context)
707         return True
708
709     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):
710         """
711         action :('schedule','Schedule a call'), ('log','Log a call')
712         """
713         phonecall = self.pool.get('crm.phonecall')
714         model_data = self.pool.get('ir.model.data')
715         phonecall_dict = {}
716         if not categ_id:
717             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
718             if res_id:
719                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
720         for lead in self.browse(cr, uid, ids, context=context):
721             if not section_id:
722                 section_id = lead.section_id and lead.section_id.id or False
723             if not user_id:
724                 user_id = lead.user_id and lead.user_id.id or False
725             vals = {
726                     'name' : call_summary,
727                     'opportunity_id' : lead.id,
728                     'user_id' : user_id or False,
729                     'categ_id' : categ_id or False,
730                     'description' : desc or '',
731                     'date' : schedule_time,
732                     'section_id' : section_id or False,
733                     'partner_id': lead.partner_id and lead.partner_id.id or False,
734                     'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
735                     'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
736                     'priority': lead.priority,
737             }
738             new_id = phonecall.create(cr, uid, vals, context=context)
739             phonecall.case_open(cr, uid, [new_id], context=context)
740             if action == 'log':
741                 phonecall.case_close(cr, uid, [new_id], context=context)
742             phonecall_dict[lead.id] = new_id
743             self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
744         return phonecall_dict
745
746
747     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
748         models_data = self.pool.get('ir.model.data')
749
750         # Get Opportunity views
751         form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
752         tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
753         return {
754                 'name': _('Opportunity'),
755                 'view_type': 'form',
756                 'view_mode': 'tree, form',
757                 'res_model': 'crm.lead',
758                 'domain': [('type', '=', 'opportunity')],
759                 'res_id': int(opportunity_id),
760                 'view_id': False,
761                 'views': [(form_view and form_view[1] or False, 'form'),
762                           (tree_view and tree_view[1] or False, 'tree'),
763                           (False, 'calendar'), (False, 'graph')],
764                 'type': 'ir.actions.act_window',
765         }
766
767     def action_makeMeeting(self, cr, uid, ids, context=None):
768         """ This opens Meeting's calendar view to schedule meeting on current Opportunity
769             @return : Dictionary value for created Meeting view
770         """
771         opportunity = self.browse(cr, uid, ids[0], context)
772         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
773         res['context'] = {
774             'default_opportunity_id': opportunity.id,
775             'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
776             'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
777             'default_user_id': uid,
778             'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
779             'default_email_from': opportunity.email_from,
780             'default_state': 'open',
781             'default_name': opportunity.name,
782         }
783         return res
784
785     def unlink(self, cr, uid, ids, context=None):
786         for lead in self.browse(cr, uid, ids, context):
787             if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
788                 raise osv.except_osv(_('Error!'),
789                     _("You cannot delete lead '%s' because it is not in 'Draft' state. " \
790                       "You can still cancel it, instead of deleting it.") % lead.name)
791         return super(crm_lead, self).unlink(cr, uid, ids, context)
792
793     def write(self, cr, uid, ids, vals, context=None):
794         if vals.get('stage_id') and not vals.get('probability'):
795             # change probability of lead(s) if required by stage
796             stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
797             if stage.on_change:
798                 vals['probability'] = stage.probability
799         if vals.get('section_id'):
800             section_id = self.pool.get('crm.case.section').browse(cr, uid, vals.get('section_id'), context=context)
801             if section_id:
802                 vals.setdefault('message_follower_ids', [])
803                 vals['message_follower_ids'] += [(4, follower.id) for follower in section_id.message_follower_ids]
804         return super(crm_lead,self).write(cr, uid, ids, vals, context) 
805
806     # ----------------------------------------
807     # Mail Gateway
808     # ----------------------------------------
809
810     def message_new(self, cr, uid, msg, custom_values=None, context=None):
811         """ Overrides mail_thread message_new that is called by the mailgateway
812             through message_process.
813             This override updates the document according to the email.
814         """
815         if custom_values is None: custom_values = {}
816         custom_values.update({
817             'name':  msg.get('subject') or _("No Subject"),
818             'description': msg.get('body'),
819             'email_from': msg.get('from'),
820             'email_cc': msg.get('cc'),
821             'user_id': False,
822         })
823         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
824             custom_values['priority'] = msg.get('priority')
825         return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
826
827     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
828         """ Overrides mail_thread message_update that is called by the mailgateway
829             through message_process.
830             This method updates the document according to the email.
831         """
832         if isinstance(ids, (str, int, long)):
833             ids = [ids]
834         if update_vals is None: update_vals = {}
835
836         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
837             update_vals['priority'] = msg.get('priority')
838         maps = {
839             'cost':'planned_cost',
840             'revenue': 'planned_revenue',
841             'probability':'probability',
842         }
843         for line in msg.get('body', '').split('\n'):
844             line = line.strip()
845             res = tools.misc.command_re.match(line)
846             if res and maps.get(res.group(1).lower()):
847                 key = maps.get(res.group(1).lower())
848                 update_vals[key] = res.group(2).lower()
849
850         return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
851
852     # ----------------------------------------
853     # OpenChatter methods and notifications
854     # ----------------------------------------
855
856     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
857         """ Override of the (void) default notification method. """
858         stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
859         return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
860
861     def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
862         if isinstance(lead, (int, long)):
863             lead = self.browse(cr, uid, [lead], context=context)[0]
864         return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
865
866     def create_send_note(self, cr, uid, ids, context=None):
867         for id in ids:
868             message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
869             self.message_post(cr, uid, [id], body=message, context=context)
870         return True
871
872     def case_mark_lost_send_note(self, cr, uid, ids, context=None):
873         message = _("Opportunity has been <b>lost</b>.")
874         return self.message_post(cr, uid, ids, body=message, context=context)
875
876     def case_mark_won_send_note(self, cr, uid, ids, context=None):
877         message = _("Opportunity has been <b>won</b>.")
878         return self.message_post(cr, uid, ids, body=message, context=context)
879
880     def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
881         phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
882         if action == 'log': prefix = 'Logged'
883         else: prefix = 'Scheduled'
884         message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
885         return self.message_post(cr, uid, ids, body=message, context=context)
886
887     def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
888         for lead in self.browse(cr, uid, ids, context=context):
889             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))
890             lead.message_post(body=message)
891         return True
892
893     def convert_opportunity_send_note(self, cr, uid, lead, context=None):
894         message = _("Lead has been <b>converted to an opportunity</b>.")
895         lead.message_post(body=message)
896         return True
897
898 crm_lead()
899
900 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: