[IMP] crm.lead: overridden name_get to hide the inherited res.partner.address behavior
[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
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'),
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     }
190
191     _defaults = {
192         'active': lambda *a: 1,
193         'user_id': crm_case._get_default_user,
194         'email_from': crm_case._get_default_email,
195         'state': lambda *a: 'draft',
196         'type': lambda *a: 'lead',
197         'section_id': crm_case._get_section,
198         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
199         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
200         #'stage_id': _get_stage_id,
201     }
202
203     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
204         """This function returns value of partner email based on Partner Address
205         """
206         if not add:
207             return {'value': {'email_from': False, 'country_id': False}}
208         address = self.pool.get('res.partner.address').browse(cr, uid, add)
209         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
210
211     def on_change_optin(self, cr, uid, ids, optin):
212         return {'value':{'optin':optin,'optout':False}}
213
214     def on_change_optout(self, cr, uid, ids, optout):
215         return {'value':{'optout':optout,'optin':False}}
216
217     def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
218         if not stage_id:
219             return {'value':{}}
220         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
221         if not stage.on_change:
222             return {'value':{}}
223         return {'value':{'probability': stage.probability}}
224
225     def stage_find_percent(self, cr, uid, percent, section_id):
226         """ Return the first stage with a probability == percent
227         """
228         stage_pool = self.pool.get('crm.case.stage')
229         if section_id :
230             ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
231         else :
232             ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
233
234         if ids:
235             return ids[0]
236         return False
237
238     def stage_find_lost(self, cr, uid, section_id):
239         return self.stage_find_percent(cr, uid, 0.0, section_id)
240
241     def stage_find_won(self, cr, uid, section_id):
242         return self.stage_find_percent(cr, uid, 100.0, section_id)
243
244     def case_open(self, cr, uid, ids, *args):
245         for l in self.browse(cr, uid, ids):
246             # When coming from draft override date and stage otherwise just set state
247             if l.state == 'draft':
248                 if l.type == 'lead':
249                     message = _("The lead '%s' has been opened.") % l.name
250                 elif l.type == 'opportunity':
251                     message = _("The opportunity '%s' has been opened.") % l.name
252                 else:
253                     message = _("The case '%s' has been opened.") % l.name
254                 self.log(cr, uid, l.id, message)
255                 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
256                 self.write(cr, uid, [l.id], value)
257                 if l.type == 'opportunity' and not l.stage_id:
258                     stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
259                     if stage_id:
260                         self.stage_set(cr, uid, [l.id], stage_id)
261         res = super(crm_lead, self).case_open(cr, uid, ids, *args)
262         return res
263
264     def case_close(self, cr, uid, ids, *args):
265         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
266         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
267         for case in self.browse(cr, uid, ids):
268             if case.type == 'lead':
269                 message = _("The lead '%s' has been closed.") % case.name
270             else:
271                 message = _("The case '%s' has been closed.") % case.name
272             self.log(cr, uid, case.id, message)
273         return res
274
275     def case_cancel(self, cr, uid, ids, *args):
276         """Overrides cancel for crm_case for setting probability
277         """
278         res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
279         self.write(cr, uid, ids, {'probability' : 0.0})
280         return res
281
282     def case_reset(self, cr, uid, ids, *args):
283         """Overrides reset as draft in order to set the stage field as empty
284         """
285         res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
286         self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
287         return res
288
289     def case_mark_lost(self, cr, uid, ids, *args):
290         """Mark the case as lost: state = done and probability = 0%
291         """
292         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
293         self.write(cr, uid, ids, {'probability' : 0.0})
294         for l in self.browse(cr, uid, ids):
295             stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
296             if stage_id:
297                 self.stage_set(cr, uid, [l.id], stage_id)
298             message = _("The opportunity '%s' has been marked as lost.") % l.name
299             self.log(cr, uid, l.id, message)
300         return res
301
302     def case_mark_won(self, cr, uid, ids, *args):
303         """Mark the case as lost: state = done and probability = 0%
304         """
305         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
306         self.write(cr, uid, ids, {'probability' : 100.0})
307         for l in self.browse(cr, uid, ids):
308             stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
309             if stage_id:
310                 self.stage_set(cr, uid, [l.id], stage_id)
311             message = _("The opportunity '%s' has been been won.") % l.name
312             self.log(cr, uid, l.id, message)
313         return res
314
315     def convert_opportunity(self, cr, uid, ids, context=None):
316         """ Precomputation for converting lead to opportunity
317         """
318         if context is None:
319             context = {}
320         context.update({'active_ids': ids})
321
322         data_obj = self.pool.get('ir.model.data')
323         value = {}
324
325
326         for case in self.browse(cr, uid, ids, context=context):
327             context.update({'active_id': case.id})
328             data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
329             view_id1 = False
330             if data_id:
331                 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
332             value = {
333                     'name': _('Create Partner'),
334                     'view_type': 'form',
335                     'view_mode': 'form,tree',
336                     'res_model': 'crm.lead2opportunity.partner',
337                     'view_id': False,
338                     'context': context,
339                     'views': [(view_id1, 'form')],
340                     'type': 'ir.actions.act_window',
341                     'target': 'new',
342                     'nodestroy': True
343             }
344         return value
345
346     def message_new(self, cr, uid, msg, custom_values=None, context=None):
347         """Automatically calls when new email message arrives"""
348         res_id = super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
349         subject = msg.get('subject')  or _("No Subject")
350         body = msg.get('body_text')
351
352         msg_from = msg.get('from')
353         priority = msg.get('priority')
354         vals = {
355             'name': subject,
356             'email_from': msg_from,
357             'email_cc': msg.get('cc'),
358             'description': body,
359             'user_id': False,
360         }
361         if priority:
362             vals['priority'] = priority
363         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
364         self.write(cr, uid, [res_id], vals, context)
365         return res_id
366
367     def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
368         if isinstance(ids, (str, int, long)):
369             ids = [ids]
370
371         super(crm_lead, self).message_update(cr, uid, ids, msg, context=context)
372
373         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
374             vals['priority'] = msg.get('priority')
375         maps = {
376             'cost':'planned_cost',
377             'revenue': 'planned_revenue',
378             'probability':'probability'
379         }
380         vls = {}
381         for line in msg['body_text'].split('\n'):
382             line = line.strip()
383             res = tools.misc.command_re.match(line)
384             if res and maps.get(res.group(1).lower()):
385                 key = maps.get(res.group(1).lower())
386                 vls[key] = res.group(2).lower()
387         vals.update(vls)
388
389         # Unfortunately the API is based on lists
390         # but we want to update the state based on the
391         # previous state, so we have to loop:
392         for case in self.browse(cr, uid, ids, context=context):
393             values = dict(vals)
394             if case.state in CRM_LEAD_PENDING_STATES:
395                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
396             res = self.write(cr, uid, [case.id], values, context=context)
397         return res
398
399     def action_makeMeeting(self, cr, uid, ids, context=None):
400         """
401         This opens Meeting's calendar view to schedule meeting on current Opportunity
402         @return : Dictionary value for created Meeting view
403         """
404         value = {}
405         for opp in self.browse(cr, uid, ids, context=context):
406             data_obj = self.pool.get('ir.model.data')
407
408             # Get meeting views
409             result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
410             res = data_obj.read(cr, uid, result, ['res_id'])
411             id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
412             id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
413             id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
414             if id1:
415                 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
416             if id2:
417                 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
418             if id3:
419                 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
420
421             context = {
422                 'default_opportunity_id': opp.id,
423                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
424                 'default_user_id': uid, 
425                 'default_section_id': opp.section_id and opp.section_id.id or False,
426                 'default_email_from': opp.email_from,
427                 'default_state': 'open',  
428                 'default_name': opp.name
429             }
430             value = {
431                 'name': _('Meetings'),
432                 'context': context,
433                 'view_type': 'form',
434                 'view_mode': 'calendar,form,tree',
435                 'res_model': 'crm.meeting',
436                 'view_id': False,
437                 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
438                 'type': 'ir.actions.act_window',
439                 'search_view_id': res['res_id'],
440                 'nodestroy': True
441             }
442         return value
443
444
445     def unlink(self, cr, uid, ids, context=None):
446         for lead in self.browse(cr, uid, ids, context):
447             if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
448                 raise osv.except_osv(_('Warning !'),
449                     _('You can not delete this lead. You should better cancel it.'))
450         return super(crm_lead, self).unlink(cr, uid, ids, context)
451
452
453     def write(self, cr, uid, ids, vals, context=None):
454         if not context:
455             context = {}
456
457         if 'date_closed' in vals:
458             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
459
460         if 'stage_id' in vals and vals['stage_id']:
461             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
462             text = _("Changed Stage to: %s") % stage_obj.name
463             self.message_append(cr, uid, ids, text, body_text=text, context=context)
464             message=''
465             for case in self.browse(cr, uid, ids, context=context):
466                 if case.type == 'lead' or  context.get('stage_type',False)=='lead':
467                     message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
468                 elif case.type == 'opportunity':
469                     message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
470                 self.log(cr, uid, case.id, message)
471         return super(crm_lead,self).write(cr, uid, ids, vals, context)
472
473 crm_lead()
474
475 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: