[MERGE] remove stage type ksa, and MASSIVE crm_lead cleanup
[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 = ['mailgate.thread','res.partner.address']
44
45     def _compute_day(self, cr, uid, ids, fields, args, context=None):
46         """
47         @param cr: the current row, from the database cursor,
48         @param uid: the current user’s ID for security checks,
49         @param ids: List of Openday’s IDs
50         @return: difference between current date and log date
51         @param context: A standard dictionary for contextual values
52         """
53         cal_obj = self.pool.get('resource.calendar')
54         res_obj = self.pool.get('resource.resource')
55
56         res = {}
57         for lead in self.browse(cr, uid, ids, context=context):
58             for field in fields:
59                 res[lead.id] = {}
60                 duration = 0
61                 ans = False
62                 if field == 'day_open':
63                     if lead.date_open:
64                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
65                         date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
66                         ans = date_open - date_create
67                         date_until = lead.date_open
68                 elif field == 'day_close':
69                     if lead.date_closed:
70                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
71                         date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
72                         date_until = lead.date_closed
73                         ans = date_close - date_create
74                 if ans:
75                     resource_id = False
76                     if lead.user_id:
77                         resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
78                         if len(resource_ids):
79                             resource_id = resource_ids[0]
80
81                     duration = float(ans.days)
82                     if lead.section_id and lead.section_id.resource_calendar_id:
83                         duration =  float(ans.days) * 24
84                         new_dates = cal_obj.interval_get(cr,
85                             uid,
86                             lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
87                             datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
88                             duration,
89                             resource=resource_id
90                         )
91                         no_days = []
92                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
93                         for in_time, out_time in new_dates:
94                             if in_time.date not in no_days:
95                                 no_days.append(in_time.date)
96                             if out_time > date_until:
97                                 break
98                         duration =  len(no_days)
99                 res[lead.id][field] = abs(int(duration))
100         return res
101
102     def _history_search(self, cr, uid, obj, name, args, context=None):
103         res = []
104         msg_obj = self.pool.get('mailgate.message')
105         message_ids = msg_obj.search(cr, uid, [('history','=',True), ('name', args[0][1], args[0][2])], context=context)
106         lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
107
108         if lead_ids:
109             return [('id', 'in', lead_ids)]
110         else:
111             return [('id', '=', '0')]
112
113     def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
114         res = {}
115         for obj in self.browse(cr, uid, ids, context=context):
116             res[obj.id] = ''
117             for msg in obj.message_ids:
118                 if msg.history:
119                     res[obj.id] = msg.name
120                     break
121         return res
122
123     _columns = {
124         # Overridden from res.partner.address:
125         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
126             select=True, help="Optional linked partner, usually after conversion of the lead"),
127
128         'id': fields.integer('ID'),
129         'name': fields.char('Name', size=64, select=1),
130         'active': fields.boolean('Active', required=False),
131         'date_action_last': fields.datetime('Last Action', readonly=1),
132         'date_action_next': fields.datetime('Next Action', readonly=1),
133         'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
134         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
135                         select=True, help='When sending mails, the default email address is taken from the sales team.'),
136         'create_date': fields.datetime('Creation Date' , readonly=True),
137         '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"),
138         'description': fields.text('Notes'),
139         'write_date': fields.datetime('Update Date' , readonly=True),
140
141         'categ_id': fields.many2one('crm.case.categ', 'Category', \
142             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
143         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
144             domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
145         'channel_id': fields.many2one('res.partner.canal', 'Channel', help="From which channel (mail, direct, phone, ...) did this contact reach you?"),
146         'contact_name': fields.char('Contact Name', size=64),
147         '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),
148         'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
149         'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
150         'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
151         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
152         'date_closed': fields.datetime('Closed', readonly=True),
153         'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[(section_ids', '=', section_id)]"),
154         'user_id': fields.many2one('res.users', 'Salesman'),
155         'referred': fields.char('Referred By', size=64),
156         'date_open': fields.datetime('Opened', readonly=True),
157         'day_open': fields.function(_compute_day, string='Days to Open', \
158                                 multi='day_open', type="float", store=True),
159         'day_close': fields.function(_compute_day, string='Days to Close', \
160                                 multi='day_close', type="float", store=True),
161         'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
162                                   help='The state is set to \'Draft\', when a case is created.\
163                                   \nIf the case is in progress the state is set to \'Open\'.\
164                                   \nWhen the case is over, the state is set to \'Done\'.\
165                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
166         'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
167         'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
168
169
170         # Only used for type opportunity
171         'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"), 
172         'probability': fields.float('Probability (%)',group_operator="avg"),
173         'planned_revenue': fields.float('Expected Revenue'),
174         'ref': fields.reference('Reference', selection=crm._links_get, size=128),
175         'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
176         'phone': fields.char("Phone", size=64),
177         'date_deadline': fields.date('Expected Closing'),
178         'date_action': fields.date('Next Action Date'),
179         'title_action': fields.char('Next Action', size=64),
180         'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[(section_ids', '=', section_id)]"),
181     }
182
183     _defaults = {
184         'active': lambda *a: 1,
185         'user_id': crm_case._get_default_user,
186         'email_from': crm_case._get_default_email,
187         'state': lambda *a: 'draft',
188         'type': lambda *a: 'lead',
189         'section_id': crm_case._get_section,
190         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
191         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
192         #'stage_id': _get_stage_id,
193     }
194
195     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
196         """This function returns value of partner email based on Partner Address
197         """
198         if not add:
199             return {'value': {'email_from': False, 'country_id': False}}
200         address = self.pool.get('res.partner.address').browse(cr, uid, add)
201         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
202
203     def on_change_optin(self, cr, uid, ids, optin):
204         return {'value':{'optin':optin,'optout':False}}
205
206     def on_change_optout(self, cr, uid, ids, optout):
207         return {'value':{'optout':optout,'optin':False}}
208
209     def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
210         if not stage_id:
211             return {'value':{}}
212         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
213         if not stage.on_change:
214             return {'value':{}}
215         return {'value':{'probability': stage.probability}}
216
217     def stage_find_percent(self, cr, uid, percent, section_id):
218         """ Return the first stage with a probability == percent
219         """
220         stage_pool = self.pool.get('crm.case.stage')
221         if section_id :
222             ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
223         else :
224             ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
225
226         if ids:
227             return ids[0]
228         return False
229
230     def stage_find_lost(self, cr, uid, section_id):
231         return self.stage_find_percent(cr, uid, 0.0, section_id)
232
233     def stage_find_won(self, cr, uid, section_id):
234         return self.stage_find_percent(cr, uid, 100.0, section_id)
235
236     def case_open(self, cr, uid, ids, *args):
237         for l in self.browse(cr, uid, ids):
238             # When coming from draft override date and stage otherwise just set state
239             if l.state == 'draft':
240                 if l.type == 'lead':
241                     message = _("The lead '%s' has been opened.") % l.name
242                 elif l.type == 'opportunity':
243                     message = _("The opportunity '%s' has been opened.") % l.name
244                 else:
245                     message = _("The case '%s' has been opened.") % l.name
246                 self.log(cr, uid, l.id, message)
247                 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
248                 self.write(cr, uid, [l.id], value)
249                 if l.type == 'opportunity' and not l.stage_id:
250                     stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
251                     if stage_id:
252                         self.stage_set(cr, uid, [l.id], stage_id)
253         res = super(crm_lead, self).case_open(cr, uid, ids, *args)
254         return res
255
256     def case_close(self, cr, uid, ids, *args):
257         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
258         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
259         for case in self.browse(cr, uid, ids):
260             if case.type == 'lead':
261                 message = _("The lead '%s' has been closed.") % case.name
262             else:
263                 message = _("The case '%s' has been closed.") % case.name
264             self.log(cr, uid, case.id, message)
265         return res
266
267     def case_cancel(self, cr, uid, ids, *args):
268         """Overrides cancel for crm_case for setting probability
269         """
270         res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
271         self.write(cr, uid, ids, {'probability' : 0.0})
272         return res
273
274     def case_reset(self, cr, uid, ids, *args):
275         """Overrides reset as draft in order to set the stage field as empty
276         """
277         res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
278         self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
279         return res
280
281     def case_mark_lost(self, cr, uid, ids, *args):
282         """Mark the case as lost: state = done and probability = 0%
283         """
284         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
285         self.write(cr, uid, ids, {'probability' : 0.0})
286         for l in self.browse(cr, uid, ids):
287             stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
288             if stage_id:
289                 self.stage_set(cr, uid, [l.id], stage_id)
290             message = _("The opportunity '%s' has been marked as lost.") % l.name
291             self.log(cr, uid, l.id, message)
292         return res
293
294     def case_mark_won(self, cr, uid, ids, *args):
295         """Mark the case as lost: state = done and probability = 0%
296         """
297         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
298         self.write(cr, uid, ids, {'probability' : 100.0})
299         for l in self.browse(cr, uid, ids):
300             stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
301             if stage_id:
302                 self.stage_set(cr, uid, [l.id], stage_id)
303             message = _("The opportunity '%s' has been been won.") % l.name
304             self.log(cr, uid, l.id, message)
305         return res
306
307     def convert_opportunity(self, cr, uid, ids, context=None):
308         """ Precomputation for converting lead to opportunity
309         """
310         if context is None:
311             context = {}
312         context.update({'active_ids': ids})
313
314         data_obj = self.pool.get('ir.model.data')
315         value = {}
316
317
318         for case in self.browse(cr, uid, ids, context=context):
319             context.update({'active_id': case.id})
320             data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
321             view_id1 = False
322             if data_id:
323                 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
324             value = {
325                     'name': _('Create Partner'),
326                     'view_type': 'form',
327                     'view_mode': 'form,tree',
328                     'res_model': 'crm.lead2opportunity.partner',
329                     'view_id': False,
330                     'context': context,
331                     'views': [(view_id1, 'form')],
332                     'type': 'ir.actions.act_window',
333                     'target': 'new',
334                     'nodestroy': True
335             }
336         return value
337
338     def message_new(self, cr, uid, msg, context=None):
339         """ Automatically calls when new email message arrives
340         """
341         mailgate_pool = self.pool.get('email.server.tools')
342
343         subject = msg.get('subject') or _("No Subject")
344         body = msg.get('body')
345         msg_from = msg.get('from')
346         priority = msg.get('priority')
347
348         vals = {
349             'name': subject,
350             'email_from': msg_from,
351             'email_cc': msg.get('cc'),
352             'description': body,
353             'user_id': False,
354         }
355         if msg.get('priority', False):
356             vals['priority'] = priority
357
358         res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
359         if res:
360             vals.update(res)
361
362         res = self.create(cr, uid, vals, context)
363         attachents = msg.get('attachments', [])
364         for attactment in attachents or []:
365             data_attach = {
366                 'name': attactment,
367                 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
368                 'datas_fname': attactment,
369                 'description': 'Mail attachment',
370                 'res_model': self._name,
371                 'res_id': res,
372             }
373             self.pool.get('ir.attachment').create(cr, uid, data_attach)
374
375         return res
376
377     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
378         """
379         @param ids: List of update mail’s IDs
380         """
381         if isinstance(ids, (str, int, long)):
382             ids = [ids]
383
384         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
385             vals['priority'] = msg.get('priority')
386
387         maps = {
388             'cost':'planned_cost',
389             'revenue': 'planned_revenue',
390             'probability':'probability'
391         }
392         vls = {}
393         for line in msg['body'].split('\n'):
394             line = line.strip()
395             res = tools.misc.command_re.match(line)
396             if res and maps.get(res.group(1).lower()):
397                 key = maps.get(res.group(1).lower())
398                 vls[key] = res.group(2).lower()
399         vals.update(vls)
400
401         # Unfortunately the API is based on lists
402         # but we want to update the state based on the
403         # previous state, so we have to loop:
404         for case in self.browse(cr, uid, ids, context=context):
405             values = dict(vals)
406             if case.state in CRM_LEAD_PENDING_STATES:
407                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
408             res = self.write(cr, uid, [case.id], values, context=context)
409         return res
410
411     def msg_send(self, cr, uid, id, *args, **argv):
412         """ Send The Message
413         @param ids: List of email’s IDs
414         """
415         return True
416
417     def action_makeMeeting(self, cr, uid, ids, context=None):
418         """
419         This opens Meeting's calendar view to schedule meeting on current Opportunity
420         @return : Dictionary value for created Meeting view
421         """
422         value = {}
423         for opp in self.browse(cr, uid, ids, context=context):
424             data_obj = self.pool.get('ir.model.data')
425
426             # Get meeting views
427             result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
428             res = data_obj.read(cr, uid, result, ['res_id'])
429             id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
430             id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
431             id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
432             if id1:
433                 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
434             if id2:
435                 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
436             if id3:
437                 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
438
439             context = {
440                 'default_opportunity_id': opp.id,
441                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
442                 'default_user_id': uid, 
443                 'default_section_id': opp.section_id and opp.section_id.id or False,
444                 'default_email_from': opp.email_from,
445                 'default_state': 'open',  
446                 'default_name': opp.name
447             }
448             value = {
449                 'name': _('Meetings'),
450                 'context': context,
451                 'view_type': 'form',
452                 'view_mode': 'calendar,form,tree',
453                 'res_model': 'crm.meeting',
454                 'view_id': False,
455                 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
456                 'type': 'ir.actions.act_window',
457                 'search_view_id': res['res_id'],
458                 'nodestroy': True
459             }
460         return value
461
462     def write(self, cr, uid, ids, vals, context=None):
463         if not context:
464             context = {}
465
466         if 'date_closed' in vals:
467             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
468
469         if 'stage_id' in vals and vals['stage_id']:
470             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
471             self.history(cr, uid, ids, _("Changed Stage to: %s") % stage_obj.name, details=_("Changed Stage to: %s") % stage_obj.name)
472             message=''
473             for case in self.browse(cr, uid, ids, context=context):
474                 if case.type == 'lead' or  context.get('stage_type',False)=='lead':
475                     message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
476                 elif case.type == 'opportunity':
477                     message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
478                 self.log(cr, uid, case.id, message)
479         return super(crm_lead,self).write(cr, uid, ids, vals, context)
480
481     def unlink(self, cr, uid, ids, context=None):
482         for lead in self.browse(cr, uid, ids, context):
483             if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
484                 raise osv.except_osv(_('Warning !'),
485                     _('You can not delete this lead. You should better cancel it.'))
486         return super(crm_lead, self).unlink(cr, uid, ids, context)
487
488
489 crm_lead()
490
491 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: