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