[MERGE]
[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"
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     _columns = {
102         # Overridden from res.partner.address:
103         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', 
104             select=True, help="Optional linked partner, usually after conversion of the lead"),
105
106         # From crm.case
107         'id': fields.integer('ID'),
108         'name': fields.char('Name', size=64),
109         'active': fields.boolean('Active', required=False),
110         'date_action_last': fields.datetime('Last Action', readonly=1),
111         'date_action_next': fields.datetime('Next Action', readonly=1),
112         'email_from': fields.char('Email', size=128, help="E-mail address of the contact"),
113         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
114                         select=True, help='Sales team to which this case belongs to. Defines responsible user and e-mail address for the mail gateway.'),
115         'create_date': fields.datetime('Creation Date' , readonly=True),
116         '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"),
117         'description': fields.text('Notes'),
118         'write_date': fields.datetime('Update Date' , readonly=True),
119
120         # Lead fields
121         'categ_id': fields.many2one('crm.case.categ', 'Category', \
122             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
123         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
124             domain="['|',('section_id','=',section_id),('section_id','=',False)]"),
125         'channel_id': fields.many2one('res.partner.canal', 'Channel'),
126
127         'contact_name': fields.char('Contact Name', size=64), 
128         'partner_name': fields.char("Customer Name", size=64),
129         'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
130         'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
131         'type':fields.selection([
132             ('lead','Lead'),
133             ('opportunity','Opportunity'),
134
135         ],'Type', help="Type is used to separate Leads and Opportunities"),
136         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
137         'date_closed': fields.datetime('Closed', readonly=True),
138         'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('type','=','lead')]"),
139         'user_id': fields.many2one('res.users', 'Salesman',help='By Default Salesman is Administrator when create New User'),
140         'referred': fields.char('Referred By', size=64),
141         'date_open': fields.datetime('Opened', readonly=True),
142         'day_open': fields.function(_compute_day, string='Days to Open', \
143                                 method=True, multi='day_open', type="float", store=True),
144         'day_close': fields.function(_compute_day, string='Days to Close', \
145                                 method=True, multi='day_close', type="float", store=True),
146         'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
147                                   help='The state is set to \'Draft\', when a case is created.\
148                                   \nIf the case is in progress the state is set to \'Open\'.\
149                                   \nWhen the case is over, the state is set to \'Done\'.\
150                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'), 
151         'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
152     }
153     
154     def _get_stage_id(self, cr, uid, context=None):
155         """Finds type of stage according to object.
156         @param self: The object pointer
157         @param cr: the current row, from the database cursor,
158         @param uid: the current user’s ID for security checks,
159         @param context: A standard dictionary for contextual values
160         """
161         if context is None:
162             context = {}
163         type = context and context.get('stage_type', '')
164         stage_ids = self.pool.get('crm.case.stage').search(cr, uid, [('type','=',type),('sequence','>=',1)])
165         return stage_ids and stage_ids[0] or False
166
167     _defaults = {
168         'active': lambda *a: 1,
169         'user_id': crm_case._get_default_user,
170         'email_from': crm_case._get_default_email,
171         'state': lambda *a: 'draft',
172         'type': lambda *a: 'lead',
173         'section_id': crm_case._get_section,
174         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
175         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
176         'stage_id': _get_stage_id,
177     }
178
179     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
180         """This function returns value of partner email based on Partner Address
181         @param self: The object pointer
182         @param cr: the current row, from the database cursor,
183         @param uid: the current user’s ID for security checks,
184         @param ids: List of case IDs
185         @param add: Id of Partner's address
186         @email: Partner's email ID
187         """
188         if not add:
189             return {'value': {'email_from': False, 'country_id': False}}
190         address = self.pool.get('res.partner.address').browse(cr, uid, add)
191         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
192
193     def case_open(self, cr, uid, ids, *args):
194         """Overrides cancel for crm_case for setting Open Date
195         @param self: The object pointer
196         @param cr: the current row, from the database cursor,
197         @param uid: the current user’s ID for security checks,
198         @param ids: List of case's Ids
199         @param *args: Give Tuple Value
200         """
201         old_state = self.read(cr, uid, ids, ['state'])[0]['state']
202         old_stage_id = self.read(cr, uid, ids, ['stage_id'])[0]['stage_id']
203         res = super(crm_lead, self).case_open(cr, uid, ids, *args)
204         if old_state == 'draft':
205             value = {}
206             if not old_stage_id:
207                 stage_id = super(crm_lead, self).stage_next(cr, uid, ids, *args)
208                 if stage_id:
209                     value.update(self.onchange_stage_id(cr, uid, ids, stage_id, context={})['value'])
210             value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
211             self.write(cr, uid, ids, value)
212
213         for case in self.browse(cr, uid, ids):
214             if case.type == 'lead':
215                 message = _("The lead '%s' has been opened.") % case.name
216             elif case.type == 'opportunity':
217                 message = _("The opportunity '%s' has been opened.") % case.name
218             else:
219                 message = _("The case '%s' has been opened.") % case.name
220             self.log(cr, uid, case.id, message)
221         return res
222
223     def case_close(self, cr, uid, ids, *args):
224         """Overrides close for crm_case for setting close date
225         @param self: The object pointer
226         @param cr: the current row, from the database cursor,
227         @param uid: the current user’s ID for security checks,
228         @param ids: List of case Ids
229         @param *args: Tuple Value for additional Params
230         """
231         res = super(crm_lead, self).case_close(cr, uid, ids, args)
232         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
233         for case in self.browse(cr, uid, ids):
234             if case.type == 'lead':
235                 message = _("The lead '%s' has been closed.") % case.name
236             elif case.type == 'opportunity':
237                 message = _("The opportunity '%s' has been closed.") % case.name
238             else:
239                 message = _("The case '%s' has been closed.") % case.name
240             self.log(cr, uid, case.id, message)
241         return res
242
243     def convert_opportunity(self, cr, uid, ids, context=None):
244         """ Precomputation for converting lead to opportunity
245         @param cr: the current row, from the database cursor,
246         @param uid: the current user’s ID for security checks,
247         @param ids: List of closeday’s IDs
248         @param context: A standard dictionary for contextual values
249         @return: Value of action in dict
250         """
251         if context is None:
252             context = {}
253         context.update({'active_ids': ids})
254
255         data_obj = self.pool.get('ir.model.data')
256         data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_action')
257         value = {}
258
259         view_id = False
260         if data_id:
261             view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
262
263         for case in self.browse(cr, uid, ids):
264             context.update({'active_id': case.id})
265             if not case.partner_id:
266                 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
267                 view_id1 = False
268                 if data_id:
269                     view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
270                 value = {
271                         'name': _('Create Partner'),
272                         'view_type': 'form',
273                         'view_mode': 'form,tree',
274                         'res_model': 'crm.lead2opportunity.partner',
275                         'view_id': False,
276                         'context': context,
277                         'views': [(view_id1, 'form')],
278                         'type': 'ir.actions.act_window',
279                         'target': 'new',
280                         'nodestroy': True
281                         }
282                 break
283             else:
284                 value = {
285                         'name': _('Create Opportunity'),
286                         'view_type': 'form',
287                         'view_mode': 'form,tree',
288                         'res_model': 'crm.lead2opportunity.action',
289                         'view_id': False,
290                         'context': context,
291                         'views': [(view_id, 'form')],
292                         'type': 'ir.actions.act_window',
293                         'target': 'new',
294                         'nodestroy': True
295                         }
296         return value
297
298     def write(self, cr, uid, ids, vals, context=None):
299         if 'date_closed' in vals:
300             return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
301             
302         if 'stage_id' in vals and vals['stage_id']:
303             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
304             self.history(cr, uid, ids, _('Stage'), details=stage_obj.name)
305             for case in self.browse(cr, uid, ids, context=context):
306                 if case.type == 'lead':
307                     message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, case.stage_id.name)
308                 elif case.type == 'opportunity':
309                     message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, case.stage_id.name)
310                 self.log(cr, uid, case.id, message)
311         return super(crm_lead,self).write(cr, uid, ids, vals, context)
312     
313     def stage_next(self, cr, uid, ids, context=None):
314         stage = super(crm_lead, self).stage_next(cr, uid, ids, context=context)
315         if stage:
316             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
317             if stage_obj.on_change:
318                 data = {'probability': stage_obj.probability}
319                 self.write(cr, uid, ids, data)
320         return stage
321     
322     def message_new(self, cr, uid, msg, context=None):
323         """
324         Automatically calls when new email message arrives
325
326         @param self: The object pointer
327         @param cr: the current row, from the database cursor,
328         @param uid: the current user’s ID for security checks
329         """
330         mailgate_pool = self.pool.get('email.server.tools')
331
332         subject = msg.get('subject')
333         body = msg.get('body')
334         msg_from = msg.get('from')
335         priority = msg.get('priority')
336
337         vals = {
338             'name': subject,
339             'email_from': msg_from,
340             'email_cc': msg.get('cc'),
341             'description': body,
342             'user_id': False,
343         }
344         if msg.get('priority', False):
345             vals['priority'] = priority
346
347         res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
348         if res:
349             vals.update(res)
350
351         res = self.create(cr, uid, vals, context)
352         attachents = msg.get('attachments', [])
353         for attactment in attachents or []:
354             data_attach = {
355                 'name': attactment,
356                 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
357                 'datas_fname': attactment,
358                 'description': 'Mail attachment',
359                 'res_model': self._name,
360                 'res_id': res,
361             }
362             self.pool.get('ir.attachment').create(cr, uid, data_attach)
363
364         return res
365
366     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
367         """
368         @param self: The object pointer
369         @param cr: the current row, from the database cursor,
370         @param uid: the current user’s ID for security checks,
371         @param ids: List of update mail’s IDs 
372         """
373         if isinstance(ids, (str, int, long)):
374             ids = [ids]
375
376         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
377             vals['priority'] = msg.get('priority')
378
379         maps = {
380             'cost':'planned_cost',
381             'revenue': 'planned_revenue',
382             'probability':'probability'
383         }
384         vls = {}
385         for line in msg['body'].split('\n'):
386             line = line.strip()
387             res = tools.misc.command_re.match(line)
388             if res and maps.get(res.group(1).lower()):
389                 key = maps.get(res.group(1).lower())
390                 vls[key] = res.group(2).lower()
391         vals.update(vls)
392
393         # Unfortunately the API is based on lists
394         # but we want to update the state based on the
395         # previous state, so we have to loop:
396         for case in self.browse(cr, uid, ids, context=context):
397             values = dict(vals)
398             if case.state in CRM_LEAD_PENDING_STATES:
399                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
400             res = self.write(cr, uid, [case.id], values, context=context)
401
402         return res
403
404     def msg_send(self, cr, uid, id, *args, **argv):
405
406         """ Send The Message
407             @param self: The object pointer
408             @param cr: the current row, from the database cursor,
409             @param uid: the current user’s ID for security checks,
410             @param ids: List of email’s IDs
411             @param *args: Return Tuple Value
412             @param **args: Return Dictionary of Keyword Value
413         """
414         return True
415 crm_lead()
416
417 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: