[FIX] minor changes for call reset notification function.
[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 = _("The lead is <b>renewed</b>.")
278             elif lead.type == 'opportunity':
279                 message = _("The 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 = _("The case has been <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 = _("The lead has been <b>opened</b>.")
300         elif lead.type == 'opportunity':
301             message = _("The opportunity has been <b>opened</b>.")
302         else:
303             message = _("The case has been <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 = _("The lead has been <b>closed</b>.")
310         elif lead[0].type == 'opportunity':
311             message = _("The opportunity has been <b>closed</b>.")
312         else:
313             message = _("The case has been <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 = _("The opportunity has been <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 = _("The opportunity has been <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 = _("The lead has been <b>cancelled</b>.")
330         elif lead[0].type == 'opportunity':
331             message = _("The opportunity has been <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 = _("The lead is <b>pending</b>.")
337         elif case[0].type == 'opportunity':
338             message = _("The opportunity is <b>pending</b>.")
339         case[0].message_append_note('' ,message)
340
341     def _case_escalate_notification(self, case, context=None):
342         message = _("The 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             if action == "schedule" :
353                 phonecall.message_append_note(cr, uid, ids, '', message, need_action_user_id=obj.user_id.id)
354
355     def case_open(self, cr, uid, ids, context=None):
356         res = super(crm_lead, self).case_open(cr, uid, ids, context)
357         for lead in self.browse(cr, uid, ids, context=context):
358             value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
359             self.write(cr, uid, [lead.id], value)
360             if lead.type == 'opportunity' and not lead.stage_id:
361                 stage_id = self.stage_find(cr, uid, lead.section_id.id or False, [('sequence','>',0)])
362                 if stage_id:
363                     self.stage_set(cr, uid, [lead.id], stage_id)
364         return res
365
366     def case_close(self, cr, uid, ids, context=None):
367         res = super(crm_lead, self).case_close(cr, uid, ids, context)
368         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
369         return res
370
371     def case_cancel(self, cr, uid, ids, context=None):
372         """Overrides cancel for crm_case for setting probability
373         """
374         res = super(crm_lead, self).case_cancel(cr, uid, ids, context)
375         self.write(cr, uid, ids, {'probability' : 0.0})
376         return res
377
378     def case_reset(self, cr, uid, ids, context=None):
379         """Overrides reset as draft in order to set the stage field as empty
380         """
381         res = super(crm_lead, self).case_reset(cr, uid, ids, context)
382         self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
383         self._case_reset_notification(cr, uid, ids, context=context)
384         return res
385
386     def case_mark_lost(self, cr, uid, ids, context=None):
387         """Mark the case as lost: state = done and probability = 0%
388         """
389         res = super(crm_lead, self).case_close(cr, uid, ids, context)
390         self.write(cr, uid, ids, {'probability' : 0.0})
391         for lead in self.browse(cr, uid, ids):
392             stage_id = self.stage_find_lost(cr, uid, lead.section_id.id or False)
393             if stage_id:
394                 self.stage_set(cr, uid, [lead.id], stage_id)
395         return res
396
397     def case_mark_won(self, cr, uid, ids, context=None):
398         """Mark the case as lost: state = done and probability = 0%
399         """
400         res = super(crm_lead, self).case_close(cr, uid, ids, context=None)
401         self.write(cr, uid, ids, {'probability' : 100.0})
402         for lead in self.browse(cr, uid, ids):
403             stage_id = self.stage_find_won(cr, uid, lead.section_id.id or False)
404             if stage_id:
405                 self.stage_set(cr, uid, [lead.id], stage_id)
406             self._case_mark_won_notification(lead, context=context)
407         return res
408
409     def set_priority(self, cr, uid, ids, priority):
410         """Set lead priority
411         """
412         return self.write(cr, uid, ids, {'priority' : priority})
413
414     def set_high_priority(self, cr, uid, ids, context=None):
415         """Set lead priority to high
416         """
417         return self.set_priority(cr, uid, ids, '1')
418
419     def set_normal_priority(self, cr, uid, ids, context=None):
420         """Set lead priority to normal
421         """
422         return self.set_priority(cr, uid, ids, '3')
423
424
425     def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
426         # prepare opportunity data into dictionary for merging
427         opportunities = self.browse(cr, uid, ids, context=context)
428         def _get_first_not_null(attr):
429             if hasattr(oldest, attr):
430                 return getattr(oldest, attr)
431             for opportunity in opportunities:
432                 if hasattr(opportunity, attr):
433                     return getattr(opportunity, attr)
434             return False
435
436         def _get_first_not_null_id(attr):
437             res = _get_first_not_null(attr)
438             return res and res.id or False
439
440         def _concat_all(attr):
441             return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
442
443         data = {}
444         for field_name in fields:
445             field_info = self._all_columns.get(field_name)
446             if field_info is None:
447                 continue
448             field = field_info.column
449             if field._type in ('many2many', 'one2many'):
450                 continue
451             elif field._type == 'many2one':
452                 data[field_name] = _get_first_not_null_id(field_name)  # !!
453             elif field._type == 'text':
454                 data[field_name] = _concat_all(field_name)  #not lost
455             else:
456                 data[field_name] = _get_first_not_null(field_name)  #not lost
457         return data
458
459     def _merge_find_oldest(self, cr, uid, ids, context=None):
460         if context is None:
461             context = {}
462         #TOCHECK: where pass 'convert' in context ?
463         if context.get('convert'):
464             ids = list(set(ids) - set(context.get('lead_ids', False)) )
465
466         #search opportunities order by create date
467         opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
468         oldest_id = opportunity_ids[0]
469         return self.browse(cr, uid, oldest_id, context=context)
470
471     def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
472         body = []
473         if title:
474             body.append("%s\n" % (title))
475         for field_name in fields:
476             field_info = self._all_columns.get(field_name)
477             if field_info is None:
478                 continue
479             field = field_info.column
480             value = None
481
482             if field._type == 'selection':
483                 if hasattr(field.selection, '__call__'):
484                     key = field.selection(self, cr, uid, context=context)
485                 else:
486                     key = field.selection
487                 value = dict(key).get(lead[field_name], lead[field_name])
488             elif field._type == 'many2one':
489                 if lead[field_name]:
490                     value = lead[field_name].name_get()[0][1]
491             else:
492                 value = lead[field_name]
493
494             body.append("%s: %s" % (field.string, value or ''))
495         return "\n".join(body + ['---'])
496
497     def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
498         #TOFIX: mail template should be used instead of fix body, subject text
499         details = []
500         merge_message = _('Merged opportunities')
501         subject = [merge_message]
502         fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_id', 'channel_id', 'company_id', 'contact_name',
503                   'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
504                   'country_id', 'city', 'street', 'street2', 'zip']
505         for opportunity in opportunities:
506             subject.append(opportunity.name)
507             title = "%s : %s" % (merge_message, opportunity.name)
508             details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
509
510         subject = subject[0] + ", ".join(subject[1:])
511         details = "\n\n".join(details)
512         return opportunity.message_append_note(subject, body=details, need_action_user_id=opportunity.user_id.id)
513
514     def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
515         message = self.pool.get('mail.message')
516         for opportunity in opportunities:
517             for history in opportunity.message_ids:
518                 message.write(cr, uid, history.id, {
519                         'res_id': opportunity_id,
520                         'subject' : _("From %s : %s") % (opportunity.name, history.subject)
521                 }, context=context)
522
523         return True
524
525     def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
526         attachment = self.pool.get('ir.attachment')
527
528         # return attachments of opportunity
529         def _get_attachments(opportunity_id):
530             attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
531             return attachment.browse(cr, uid, attachment_ids, context=context)
532
533         count = 1
534         first_attachments = _get_attachments(opportunity_id)
535         for opportunity in opportunities:
536             attachments = _get_attachments(opportunity.id)
537             for first in first_attachments:
538                 for attachment in attachments:
539                     if attachment.name == first.name:
540                         values = dict(
541                             name = "%s (%s)" % (attachment.name, count,),
542                             res_id = opportunity_id,
543                         )
544                         attachment.write(values)
545                         count+=1
546
547         return True
548
549     def merge_opportunity(self, cr, uid, ids, context=None):
550         """
551         To merge opportunities
552             :param ids: list of opportunities ids to merge
553         """
554         if context is None: context = {}
555
556         #TOCHECK: where pass lead_ids in context?
557         lead_ids = context and context.get('lead_ids', []) or []
558
559         if len(ids) <= 1:
560             raise osv.except_osv(_('Warning !'),_('Please select more than one opportunity from the list view.'))
561
562         ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
563         opportunities = self.browse(cr, uid, ids, context=context)
564         opportunities_list = list(set(opportunities) - set(ctx_opportunities))
565         oldest = self._merge_find_oldest(cr, uid, ids, context=context)
566         if ctx_opportunities :
567             first_opportunity = ctx_opportunities[0]
568             tail_opportunities = opportunities_list
569         else:
570             first_opportunity = opportunities_list[0]
571             tail_opportunities = opportunities_list[1:]
572
573         fields = ['partner_id', 'title', 'name', 'categ_id', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id',
574             'partner_address_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
575             'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
576             'date_action_next', 'email_from', 'email_cc', 'partner_name']
577
578         data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
579
580         # merge data into first opportunity
581         self.write(cr, uid, [first_opportunity.id], data, context=context)
582
583         #copy message and attachements into the first opportunity
584         self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
585         self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
586
587         #Notification about loss of information
588         self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
589         #delete tail opportunities
590         self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
591
592         #open first opportunity
593         self.case_open(cr, uid, [first_opportunity.id])
594         return first_opportunity.id
595
596     def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
597         crm_stage = self.pool.get('crm.case.stage')
598         contact_id = False
599         if customer:
600             contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
601         if not section_id:
602             section_id = lead.section_id and lead.section_id.id or False
603         if section_id:
604             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
605         else:
606             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
607         stage_id = stage_ids and stage_ids[0] or False
608         return {
609                 'planned_revenue': lead.planned_revenue,
610                 'probability': lead.probability,
611                 'name': lead.name,
612                 'partner_id': customer and customer.id or False,
613                 'user_id': (lead.user_id and lead.user_id.id),
614                 'type': 'opportunity',
615                 'stage_id': stage_id or False,
616                 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
617                 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
618                 'partner_address_id': contact_id,
619         }
620
621     def _convert_opportunity_notification(self, cr, uid, lead, context=None):
622         success_message = _("Lead is <b>converted to an opportunity</b>.")
623         lead.message_append_note(success_message ,success_message, need_action_user_id=lead.user_id.id)
624         return True
625
626     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
627         partner = self.pool.get('res.partner')
628         mail_message = self.pool.get('mail.message')
629         customer = False
630         if partner_id:
631             customer = partner.browse(cr, uid, partner_id, context=context)
632         for lead in self.browse(cr, uid, ids, context=context):
633             if lead.state in ('done', 'cancel'):
634                 continue
635             if user_ids or section_id:
636                 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
637
638             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
639             self.write(cr, uid, [lead.id], vals, context=context)
640
641             self._convert_opportunity_notification(cr, uid, lead, context=context)
642             #TOCHECK: why need to change partner details in all messages of lead ?
643             if lead.partner_id:
644                 msg_ids = [ x.id for x in lead.message_ids]
645                 mail_message.write(cr, uid, msg_ids, {
646                         'partner_id': lead.partner_id.id
647                     }, context=context)
648         return True
649
650     def _lead_create_partner(self, cr, uid, lead, context=None):
651         partner = self.pool.get('res.partner')
652         partner_id = partner.create(cr, uid, {
653                     'name': lead.partner_name or lead.contact_name or lead.name,
654                     'user_id': lead.user_id.id,
655                     'comment': lead.description,
656                     'section_id': lead.section_id.id or False,
657                     'address': []
658         })
659         return partner_id
660
661     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
662         res = False
663         res_partner = self.pool.get('res.partner')
664         if partner_id:
665             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
666             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
667             res = lead.write({'partner_id' : partner_id, 'partner_address_id': contact_id}, context=context)
668
669         return res
670
671     def _lead_create_partner_address(self, cr, uid, lead, partner_id, context=None):
672         address = self.pool.get('res.partner.address')
673         return address.create(cr, uid, {
674                     'partner_id': partner_id,
675                     'name': lead.contact_name,
676                     'phone': lead.phone,
677                     'mobile': lead.mobile,
678                     'email': lead.email_from and to_email(lead.email_from)[0],
679                     'fax': lead.fax,
680                     'title': lead.title and lead.title.id or False,
681                     'function': lead.function,
682                     'street': lead.street,
683                     'street2': lead.street2,
684                     'zip': lead.zip,
685                     'city': lead.city,
686                     'country_id': lead.country_id and lead.country_id.id or False,
687                     'state_id': lead.state_id and lead.state_id.id or False,
688                 })
689
690     def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
691         """
692         This function convert partner based on action.
693         if action is 'create', create new partner with contact and assign lead to new partner_id.
694         otherwise assign lead to specified partner_id
695         """
696         if context is None:
697             context = {}
698         partner_ids = {}
699         for lead in self.browse(cr, uid, ids, context=context):
700             if action == 'create':
701                 if not partner_id:
702                     partner_id = self._lead_create_partner(cr, uid, lead, context=context)
703                 self._lead_create_partner_address(cr, uid, lead, partner_id, context=context)
704             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
705             partner_ids[lead.id] = partner_id
706         return partner_ids
707
708     def _send_mail_to_salesman(self, cr, uid, lead, context=None):
709         """
710         Send mail to salesman with updated Lead details.
711         @ lead: browse record of 'crm.lead' object.
712         """
713         #TOFIX: mail template should be used here instead of fix subject, body text.
714         message = self.pool.get('mail.message')
715         email_to = lead.user_id and lead.user_id.user_email
716         if not email_to:
717             return False
718
719         email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.user_email or email_to
720         partner = lead.partner_id and lead.partner_id.name or lead.partner_name
721         subject = "lead %s converted into opportunity" % lead.name
722         body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
723         return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
724
725
726     def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
727         index = 0
728         for lead_id in ids:
729             value = {}
730             if team_id:
731                 value['section_id'] = team_id
732             if index < len(user_ids):
733                 value['user_id'] = user_ids[index]
734                 index += 1
735             if value:
736                 self.write(cr, uid, [lead_id], value, context=context)
737         return True
738
739     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):
740         """
741         action :('schedule','Schedule a call'), ('log','Log a call')
742         """
743         phonecall = self.pool.get('crm.phonecall')
744         model_data = self.pool.get('ir.model.data')
745         phonecall_dict = {}
746         if not categ_id:
747             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
748             if res_id:
749                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
750         for lead in self.browse(cr, uid, ids, context=context):
751             if not section_id:
752                 section_id = lead.section_id and lead.section_id.id or False
753             if not user_id:
754                 user_id = lead.user_id and lead.user_id.id or False
755             vals = {
756                     'name' : call_summary,
757                     'opportunity_id' : lead.id,
758                     'user_id' : user_id or False,
759                     'categ_id' : categ_id or False,
760                     'description' : desc or '',
761                     'date' : schedule_time,
762                     'section_id' : section_id or False,
763                     'partner_id': lead.partner_id and lead.partner_id.id or False,
764                     'partner_address_id': lead.partner_address_id and lead.partner_address_id.id or False,
765                     'partner_phone' : phone or lead.phone or (lead.partner_address_id and lead.partner_address_id.phone or False),
766                     'partner_mobile' : lead.partner_address_id and lead.partner_address_id.mobile or False,
767                     'priority': lead.priority,
768             }
769
770             new_id = phonecall.create(cr, uid, vals, context=context)
771             phonecall.case_open(cr, uid, [new_id])
772             if action == 'log':
773                 phonecall.case_close(cr, uid, [new_id])
774             phonecall_dict[lead.id] = new_id
775             self._case_phonecall_notification(cr, uid, [new_id], lead, phonecall, action, context=context)
776         return phonecall_dict
777
778
779     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
780         models_data = self.pool.get('ir.model.data')
781
782         # Get Opportunity views
783         form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
784         tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
785         return {
786                 'name': _('Opportunity'),
787                 'view_type': 'form',
788                 'view_mode': 'tree, form',
789                 'res_model': 'crm.lead',
790                 'domain': [('type', '=', 'opportunity')],
791                 'res_id': int(opportunity_id),
792                 'view_id': False,
793                 'views': [(form_view and form_view[1] or False, 'form'),
794                           (tree_view and tree_view[1] or False, 'tree'),
795                           (False, 'calendar'), (False, 'graph')],
796                 'type': 'ir.actions.act_window',
797         }
798
799
800     def message_new(self, cr, uid, msg, custom_values=None, context=None):
801         """Automatically calls when new email message arrives"""
802         res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
803         subject = msg.get('subject')  or _("No Subject")
804         body = msg.get('body_text')
805
806         msg_from = msg.get('from')
807         priority = msg.get('priority')
808         vals = {
809             'name': subject,
810             'email_from': msg_from,
811             'email_cc': msg.get('cc'),
812             'description': body,
813             'user_id': False,
814         }
815         if priority:
816             vals['priority'] = priority
817         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
818         self.write(cr, uid, [res_id], vals, context)
819         return res_id
820
821     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
822         if isinstance(ids, (str, int, long)):
823             ids = [ids]
824         if vals == None:
825             vals = {}
826         super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
827
828         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
829             vals['priority'] = msg.get('priority')
830         maps = {
831             'cost':'planned_cost',
832             'revenue': 'planned_revenue',
833             'probability':'probability'
834         }
835         vls = {}
836         for line in msg['body_text'].split('\n'):
837             line = line.strip()
838             res = tools.misc.command_re.match(line)
839             if res and maps.get(res.group(1).lower()):
840                 key = maps.get(res.group(1).lower())
841                 vls[key] = res.group(2).lower()
842         vals.update(vls)
843
844         # Unfortunately the API is based on lists
845         # but we want to update the state based on the
846         # previous state, so we have to loop:
847         for case in self.browse(cr, uid, ids, context=context):
848             values = dict(vals)
849             if case.state in CRM_LEAD_PENDING_STATES:
850                 #re-open
851                 values.update(state=crm.AVAILABLE_STATES[1][0])
852                 if not case.date_open:
853                     values['date_open'] = time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
854             res = self.write(cr, uid, [case.id], values, context=context)
855         return res
856
857     def action_makeMeeting(self, cr, uid, ids, context=None):
858         """
859         This opens Meeting's calendar view to schedule meeting on current Opportunity
860         @return : Dictionary value for created Meeting view
861         """
862         if context is None:
863             context = {}
864         value = {}
865         data_obj = self.pool.get('ir.model.data')
866         for opp in self.browse(cr, uid, ids, context=context):
867             # Get meeting views
868             tree_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_meet')
869             form_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_meet')
870             calander_view = data_obj.get_object_reference(cr, uid, 'crm', 'crm_case_calendar_view_meet')
871             search_view = data_obj.get_object_reference(cr, uid, 'crm', 'view_crm_case_meetings_filter')
872             context.update({
873                 'default_opportunity_id': opp.id,
874                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
875                 'default_user_id': uid,
876                 'default_section_id': opp.section_id and opp.section_id.id or False,
877                 'default_email_from': opp.email_from,
878                 'default_state': 'open',
879                 'default_name': opp.name
880             })
881             value = {
882                 'name': _('Meetings'),
883                 'context': context,
884                 'view_type': 'form',
885                 'view_mode': 'calendar,form,tree',
886                 'res_model': 'crm.meeting',
887                 'view_id': False,
888                 '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')],
889                 'type': 'ir.actions.act_window',
890                 'search_view_id': search_view and search_view[1] or False,
891                 'nodestroy': True
892             }
893         return value
894
895
896     def unlink(self, cr, uid, ids, context=None):
897         for lead in self.browse(cr, uid, ids, context):
898             if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
899                 raise osv.except_osv(_('Error'),
900                     _("You cannot delete lead '%s'; it must be in state 'Draft' to be deleted. " \
901                       "You should better cancel it, instead of deleting it.") % lead.name)
902         return super(crm_lead, self).unlink(cr, uid, ids, context)
903
904
905     def write(self, cr, uid, ids, vals, context=None):
906         if not context:
907             context = {}
908
909         if 'date_closed' in vals:
910             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
911
912         if vals.get('stage_id'):
913             stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
914             # change probability of lead(s) if required by stage
915             if not vals.get('probability') and stage.on_change:
916                 vals['probability'] = stage.probability
917             for case in self.browse(cr, uid, ids, context=context):
918                 if case.type == 'lead' or context.get('stage_type') == 'lead':
919                     message = _("The stage of lead has been changed to <b>%s</b>.") % (stage.name)
920                     case.message_append_note('', message)
921                 elif case.type == 'opportunity':
922                     message = _("The stage of opportunity has been changed to <b>%s</b>.") % (stage.name)
923                     case.message_append_note('', message)
924
925         return super(crm_lead,self).write(cr, uid, ids, vals, context)
926
927 crm_lead()
928
929 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: