[IMP]: remove stage type in lead and opportunity
[odoo/odoo.git] / addons / crm / crm_lead.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import fields, osv
23 from datetime import datetime
24 import crm
25 import time
26 from tools.translate import _
27 from crm import crm_case
28 import binascii
29 import tools
30
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 = "date_action, priority, id desc"
43     _inherit = ['mailgate.thread','res.partner.address']
44     def _compute_day(self, cr, uid, ids, fields, args, context=None):
45         """
46         @param cr: the current row, from the database cursor,
47         @param uid: the current user’s ID for security checks,
48         @param ids: List of Openday’s IDs
49         @return: difference between current date and log date
50         @param context: A standard dictionary for contextual values
51         """
52         cal_obj = self.pool.get('resource.calendar')
53         res_obj = self.pool.get('resource.resource')
54
55         res = {}
56         for lead in self.browse(cr, uid, ids, context=context):
57             for field in fields:
58                 res[lead.id] = {}
59                 duration = 0
60                 ans = False
61                 if field == 'day_open':
62                     if lead.date_open:
63                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
64                         date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
65                         ans = date_open - date_create
66                         date_until = lead.date_open
67                 elif field == 'day_close':
68                     if lead.date_closed:
69                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
70                         date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
71                         date_until = lead.date_closed
72                         ans = date_close - date_create
73                 if ans:
74                     resource_id = False
75                     if lead.user_id:
76                         resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
77                         if len(resource_ids):
78                             resource_id = resource_ids[0]
79
80                     duration = float(ans.days)
81                     if lead.section_id and lead.section_id.resource_calendar_id:
82                         duration =  float(ans.days) * 24
83                         new_dates = cal_obj.interval_get(cr,
84                             uid,
85                             lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
86                             datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
87                             duration,
88                             resource=resource_id
89                         )
90                         no_days = []
91                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
92                         for in_time, out_time in new_dates:
93                             if in_time.date not in no_days:
94                                 no_days.append(in_time.date)
95                             if out_time > date_until:
96                                 break
97                         duration =  len(no_days)
98                 res[lead.id][field] = abs(int(duration))
99         return res
100
101     def _history_search(self, cr, uid, obj, name, args, context=None):
102         res = []
103         msg_obj = self.pool.get('mailgate.message')
104         message_ids = msg_obj.search(cr, uid, [('history','=',True), ('name', args[0][1], args[0][2])], context=context)
105         lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
106
107         if lead_ids:
108             return [('id', 'in', lead_ids)]
109         else:
110             return [('id', '=', '0')]
111
112     def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
113         res = {}
114         for obj in self.browse(cr, uid, ids, context=context):
115             res[obj.id] = ''
116             for msg in obj.message_ids:
117                 if msg.history:
118                     res[obj.id] = msg.name
119                     break
120         return res
121
122     _columns = {
123         # Overridden from res.partner.address:
124         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
125             select=True, help="Optional linked partner, usually after conversion of the lead"),
126
127         # From crm.case
128         'id': fields.integer('ID'),
129         'name': fields.char('Name', size=64),
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"),
134         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
135                         select=True, help='Sales team to which this case belongs to. Defines responsible user and e-mail address for the mail gateway.'),
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         # Lead fields
142         'categ_id': fields.many2one('crm.case.categ', 'Category', \
143             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
144         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
145             domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
146         'channel_id': fields.many2one('res.partner.canal', 'Channel', help="From which channel (mail, direct, phone, ...) did this contact reach you?"),
147         'contact_name': fields.char('Contact Name', size=64),
148         'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the into opportunity'),
149         'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
150         'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
151         'type':fields.selection([
152             ('lead','Lead'),
153             ('opportunity','Opportunity'),
154
155         ],'Type', help="Type is used to separate Leads and Opportunities"),
156         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
157         'date_closed': fields.datetime('Closed', readonly=True),
158         'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[(section_ids', '=', section_id)]"),
159         'user_id': fields.many2one('res.users', 'Salesman'),
160         'referred': fields.char('Referred By', size=64),
161         'date_open': fields.datetime('Opened', readonly=True),
162         'day_open': fields.function(_compute_day, string='Days to Open', \
163                                 multi='day_open', type="float", store=True),
164         'day_close': fields.function(_compute_day, string='Days to Close', \
165                                 multi='day_close', type="float", store=True),
166         'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
167                                   help='The state is set to \'Draft\', when a case is created.\
168                                   \nIf the case is in progress the state is set to \'Open\'.\
169                                   \nWhen the case is over, the state is set to \'Done\'.\
170                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
171         'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
172         'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
173     }
174
175
176     _defaults = {
177         'active': lambda *a: 1,
178         'user_id': crm_case._get_default_user,
179         'email_from': crm_case._get_default_email,
180         'state': lambda *a: 'draft',
181         'type': lambda *a: 'lead',
182         'section_id': crm_case._get_section,
183         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
184         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
185         #'stage_id': _get_stage_id,
186     }
187
188
189
190     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
191         """This function returns value of partner email based on Partner Address
192         @param self: The object pointer
193         @param cr: the current row, from the database cursor,
194         @param uid: the current user’s ID for security checks,
195         @param ids: List of case IDs
196         @param add: Id of Partner's address
197         @email: Partner's email ID
198         """
199         if not add:
200             return {'value': {'email_from': False, 'country_id': False}}
201         address = self.pool.get('res.partner.address').browse(cr, uid, add)
202         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
203
204     def case_open(self, cr, uid, ids, *args):
205         """Overrides cancel for crm_case for setting Open Date
206         @param self: The object pointer
207         @param cr: the current row, from the database cursor,
208         @param uid: the current user’s ID for security checks,
209         @param ids: List of case's Ids
210         @param *args: Give Tuple Value
211         """
212         leads = self.browse(cr, uid, ids)
213
214
215
216         for i in xrange(0, len(ids)):
217             if leads[i].state == 'draft':
218                 value = {}
219                 if not leads[i].stage_id :
220                     stage_id = self._find_first_stage(cr, uid, leads[i].type, leads[i].section_id.id or False)
221                     value.update({'stage_id' : stage_id})
222                 value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
223                 self.write(cr, uid, [ids[i]], value)
224             self.log_open( cr, uid, leads[i])
225         res = super(crm_lead, self).case_open(cr, uid, ids, *args)
226         return res
227
228     def log_open(self, cr, uid, case):
229         if case.type == 'lead':
230             message = _("The lead '%s' has been opened.") % case.name
231         elif case.type == 'opportunity':
232             message = _("The opportunity '%s' has been opened.") % case.name
233         else:
234             message = _("The case '%s' has been opened.") % case.name
235         self.log(cr, uid, case.id, message)
236
237     def case_close(self, cr, uid, ids, *args):
238         """Overrides close for crm_case for setting close date
239         @param self: The object pointer
240         @param cr: the current row, from the database cursor,
241         @param uid: the current user’s ID for security checks,
242         @param ids: List of case Ids
243         @param *args: Tuple Value for additional Params
244         """
245         res = super(crm_lead, self).case_close(cr, uid, ids, *args)
246         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
247         for case in self.browse(cr, uid, ids):
248             if case.type == 'lead':
249                 message = _("The lead '%s' has been closed.") % case.name
250             elif case.type == 'opportunity':
251                 message = _("The opportunity '%s' has been closed.") % case.name
252             else:
253                 message = _("The case '%s' has been closed.") % case.name
254             self.log(cr, uid, case.id, message)
255         return res
256
257     def convert_opportunity(self, cr, uid, ids, context=None):
258         """ Precomputation for converting lead to opportunity
259         @param cr: the current row, from the database cursor,
260         @param uid: the current user’s ID for security checks,
261         @param ids: List of closeday’s IDs
262         @param context: A standard dictionary for contextual values
263         @return: Value of action in dict
264         """
265         if context is None:
266             context = {}
267         context.update({'active_ids': ids})
268
269         data_obj = self.pool.get('ir.model.data')
270         value = {}
271
272
273         for case in self.browse(cr, uid, ids, context=context):
274             context.update({'active_id': case.id})
275             data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
276             view_id1 = False
277             if data_id:
278                 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
279             value = {
280                     'name': _('Create Partner'),
281                     'view_type': 'form',
282                     'view_mode': 'form,tree',
283                     'res_model': 'crm.lead2opportunity.partner',
284                     'view_id': False,
285                     'context': context,
286                     'views': [(view_id1, 'form')],
287                     'type': 'ir.actions.act_window',
288                     'target': 'new',
289                     'nodestroy': True
290             }
291         return value
292
293     def write(self, cr, uid, ids, vals, context=None):
294         if not context:
295             context = {}
296
297         if 'date_closed' in vals:
298             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
299
300         if 'stage_id' in vals and vals['stage_id']:
301             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
302             self.history(cr, uid, ids, _("Changed Stage to: %s") % stage_obj.name, details=_("Changed Stage to: %s") % stage_obj.name)
303             message=''
304             for case in self.browse(cr, uid, ids, context=context):
305                 if case.type == 'lead' or  context.get('stage_type',False)=='lead':
306                     message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
307                 elif case.type == 'opportunity':
308                     message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
309                 self.log(cr, uid, case.id, message)
310         return super(crm_lead,self).write(cr, uid, ids, vals, context)
311
312     def stage_next(self, cr, uid, ids, context=None):
313         stage = super(crm_lead, self).stage_next(cr, uid, ids, context=context)
314         if stage:
315             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
316             if stage_obj.on_change:
317                 data = {'probability': stage_obj.probability}
318                 self.write(cr, uid, ids, data)
319         return stage
320
321     def stage_previous(self, cr, uid, ids, context=None):
322         stage = super(crm_lead, self).stage_previous(cr, uid, ids, context=context)
323         if stage:
324             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
325             if stage_obj.on_change:
326                 data = {'probability': stage_obj.probability}
327                 self.write(cr, uid, ids, data)
328         return stage
329
330     def unlink(self, cr, uid, ids, context=None):
331         for lead in self.browse(cr, uid, ids, context):
332             if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
333                 raise osv.except_osv(_('Warning !'),
334                     _('You can not delete this lead. You should better cancel it.'))
335         return super(crm_lead, self).unlink(cr, uid, ids, context)
336
337     def message_new(self, cr, uid, msg, context=None):
338         """
339         Automatically calls when new email message arrives
340
341         @param self: The object pointer
342         @param cr: the current row, from the database cursor,
343         @param uid: the current user’s ID for security checks
344         """
345         mailgate_pool = self.pool.get('email.server.tools')
346
347         subject = msg.get('subject') or _("No Subject")
348         body = msg.get('body')
349         msg_from = msg.get('from')
350         priority = msg.get('priority')
351
352         vals = {
353             'name': subject,
354             'email_from': msg_from,
355             'email_cc': msg.get('cc'),
356             'description': body,
357             'user_id': False,
358         }
359         if msg.get('priority', False):
360             vals['priority'] = priority
361
362         res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
363         if res:
364             vals.update(res)
365
366         res = self.create(cr, uid, vals, context)
367         attachents = msg.get('attachments', [])
368         for attactment in attachents or []:
369             data_attach = {
370                 'name': attactment,
371                 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
372                 'datas_fname': attactment,
373                 'description': 'Mail attachment',
374                 'res_model': self._name,
375                 'res_id': res,
376             }
377             self.pool.get('ir.attachment').create(cr, uid, data_attach)
378
379         return res
380
381     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
382         """
383         @param self: The object pointer
384         @param cr: the current row, from the database cursor,
385         @param uid: the current user’s ID for security checks,
386         @param ids: List of update mail’s IDs
387         """
388         if isinstance(ids, (str, int, long)):
389             ids = [ids]
390
391         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
392             vals['priority'] = msg.get('priority')
393
394         maps = {
395             'cost':'planned_cost',
396             'revenue': 'planned_revenue',
397             'probability':'probability'
398         }
399         vls = {}
400         for line in msg['body'].split('\n'):
401             line = line.strip()
402             res = tools.misc.command_re.match(line)
403             if res and maps.get(res.group(1).lower()):
404                 key = maps.get(res.group(1).lower())
405                 vls[key] = res.group(2).lower()
406         vals.update(vls)
407
408         # Unfortunately the API is based on lists
409         # but we want to update the state based on the
410         # previous state, so we have to loop:
411         for case in self.browse(cr, uid, ids, context=context):
412             values = dict(vals)
413             if case.state in CRM_LEAD_PENDING_STATES:
414                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
415             res = self.write(cr, uid, [case.id], values, context=context)
416         return res
417
418     def msg_send(self, cr, uid, id, *args, **argv):
419
420         """ Send The Message
421             @param self: The object pointer
422             @param cr: the current row, from the database cursor,
423             @param uid: the current user’s ID for security checks,
424             @param ids: List of email’s IDs
425             @param *args: Return Tuple Value
426             @param **args: Return Dictionary of Keyword Value
427         """
428         return True
429
430     def on_change_optin(self, cr, uid, ids, optin):
431         return {'value':{'optin':optin,'optout':False}}
432
433     def on_change_optout(self, cr, uid, ids, optout):
434         return {'value':{'optout':optout,'optin':False}}
435
436 crm_lead()
437
438 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: