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