[IMP] crm: Task ID-1796, historizing lead/opportunity stage changes using write metho...
[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={}):
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):
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'),
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     _defaults = {
155         'active': lambda *a: 1,
156         'user_id': crm_case._get_default_user,
157         'email_from': crm_case._get_default_email,
158         'state': lambda *a: 'draft',
159         'type': lambda *a: 'lead',
160         'section_id': crm_case._get_section,
161         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
162         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
163     }
164
165     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
166         """This function returns value of partner email based on Partner Address
167         @param self: The object pointer
168         @param cr: the current row, from the database cursor,
169         @param uid: the current user’s ID for security checks,
170         @param ids: List of case IDs
171         @param add: Id of Partner's address
172         @email: Partner's email ID
173         """
174         if not add:
175             return {'value': {'email_from': False, 'country_id': False}}
176         address = self.pool.get('res.partner.address').browse(cr, uid, add)
177         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
178
179     def case_open(self, cr, uid, ids, *args):
180         """Overrides cancel for crm_case for setting Open Date
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's Ids
185         @param *args: Give Tuple Value
186         """
187         old_state = self.read(cr, uid, ids, ['state'])[0]['state']
188         old_stage_id = self.read(cr, uid, ids, ['stage_id'])[0]['stage_id']
189         res = super(crm_lead, self).case_open(cr, uid, ids, *args)
190         if old_state == 'draft':
191             value = {}
192             if not old_stage_id:
193                 stage_id = super(crm_lead, self).stage_next(cr, uid, ids, *args)
194                 if stage_id:
195                     value.update(self.onchange_stage_id(cr, uid, ids, stage_id, context={})['value'])
196             value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
197             self.write(cr, uid, ids, value)
198
199         for case in self.browse(cr, uid, ids):
200             if case.type == 'lead':
201                 message = _("The lead '%s' has been opened.") % case.name
202             elif case.type == 'opportunity':
203                 message = _("The opportunity '%s' has been opened.") % case.name
204             else:
205                 message = _("The case '%s' has been opened.") % case.name
206             self.log(cr, uid, case.id, message)
207         return res
208
209     def case_close(self, cr, uid, ids, *args):
210         """Overrides close for crm_case for setting close date
211         @param self: The object pointer
212         @param cr: the current row, from the database cursor,
213         @param uid: the current user’s ID for security checks,
214         @param ids: List of case Ids
215         @param *args: Tuple Value for additional Params
216         """
217         res = super(crm_lead, self).case_close(cr, uid, ids, args)
218         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
219         for case in self.browse(cr, uid, ids):
220             if case.type == 'lead':
221                 message = _("The lead '%s' has been closed.") % case.name
222             elif case.type == 'opportunity':
223                 message = _("The opportunity '%s' has been closed.") % case.name
224             else:
225                 message = _("The case '%s' has been closed.") % case.name
226             self.log(cr, uid, case.id, message)
227         return res
228
229     def convert_opportunity(self, cr, uid, ids, context=None):
230         """ Precomputation for converting lead to opportunity
231         @param cr: the current row, from the database cursor,
232         @param uid: the current user’s ID for security checks,
233         @param ids: List of closeday’s IDs
234         @param context: A standard dictionary for contextual values
235         @return: Value of action in dict
236         """
237         if not context:
238             context = {}
239         context.update({'active_ids': ids})
240
241         data_obj = self.pool.get('ir.model.data')
242         data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_action')
243         value = {}
244
245         view_id = False
246         if data_id:
247             view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
248
249         for case in self.browse(cr, uid, ids):
250             context.update({'active_id': case.id})
251             if not case.partner_id:
252                 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
253                 view_id1 = False
254                 if data_id:
255                     view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
256                 value = {
257                         'name': _('Create Partner'),
258                         'view_type': 'form',
259                         'view_mode': 'form,tree',
260                         'res_model': 'crm.lead2opportunity.partner',
261                         'view_id': False,
262                         'context': context,
263                         'views': [(view_id1, 'form')],
264                         'type': 'ir.actions.act_window',
265                         'target': 'new',
266                         'nodestroy': True
267                         }
268                 break
269             else:
270                 value = {
271                         'name': _('Create Opportunity'),
272                         'view_type': 'form',
273                         'view_mode': 'form,tree',
274                         'res_model': 'crm.lead2opportunity.action',
275                         'view_id': False,
276                         'context': context,
277                         'views': [(view_id, 'form')],
278                         'type': 'ir.actions.act_window',
279                         'target': 'new',
280                         'nodestroy': True
281                         }
282         return value
283
284     def write(self, cr, uid, ids, vals, context={}):
285         if 'date_closed' in vals:
286             return super(crm_lead,self).write(cr, uid, ids, vals, context)
287             
288         if 'stage_id' in vals and vals['stage_id']:
289             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
290             self.history(cr, uid, ids, _('Stage'), details=stage_obj.name)
291             for case in self.browse(cr, uid, ids, context=context):
292                 if case.type == 'lead':
293                     message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, case.stage_id.name)
294                 elif case.type == 'opportunity':
295                     message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, case.stage_id.name)
296                 self.log(cr, uid, case.id, message)
297         return super(crm_lead,self).write(cr, uid, ids, vals, context)
298     
299     def stage_next(self, cr, uid, ids, context=None):
300         stage = super(crm_lead, self).stage_next(cr, uid, ids, context)
301         if stage:
302             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
303             if stage_obj.on_change:
304                 data = {'probability': stage_obj.probability}
305                 self.write(cr, uid, ids, data)
306         return stage
307     
308     def message_new(self, cr, uid, msg, context):
309         """
310         Automatically calls when new email message arrives
311
312         @param self: The object pointer
313         @param cr: the current row, from the database cursor,
314         @param uid: the current user’s ID for security checks
315         """
316
317         mailgate_pool = self.pool.get('email.server.tools')
318
319         subject = msg.get('subject')
320         body = msg.get('body')
321         msg_from = msg.get('from')
322         priority = msg.get('priority')
323
324         vals = {
325             'name': subject,
326             'email_from': msg_from,
327             'email_cc': msg.get('cc'),
328             'description': body,
329             'user_id': False,
330         }
331         if msg.get('priority', False):
332             vals['priority'] = priority
333
334         res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
335         if res:
336             vals.update(res)
337
338         res = self.create(cr, uid, vals, context)
339         attachents = msg.get('attachments', [])
340         for attactment in attachents or []:
341             data_attach = {
342                 'name': attactment,
343                 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
344                 'datas_fname': attactment,
345                 'description': 'Mail attachment',
346                 'res_model': self._name,
347                 'res_id': res,
348             }
349             self.pool.get('ir.attachment').create(cr, uid, data_attach)
350
351         return res
352
353     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context={}):
354         """
355         @param self: The object pointer
356         @param cr: the current row, from the database cursor,
357         @param uid: the current user’s ID for security checks,
358         @param ids: List of update mail’s IDs 
359         """
360
361         if isinstance(ids, (str, int, long)):
362             ids = [ids]
363
364         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
365             vals['priority'] = msg.get('priority')
366
367         maps = {
368             'cost':'planned_cost',
369             'revenue': 'planned_revenue',
370             'probability':'probability'
371         }
372         vls = {}
373         for line in msg['body'].split('\n'):
374             line = line.strip()
375             res = tools.misc.command_re.match(line)
376             if res and maps.get(res.group(1).lower()):
377                 key = maps.get(res.group(1).lower())
378                 vls[key] = res.group(2).lower()
379         vals.update(vls)
380
381         # Unfortunately the API is based on lists
382         # but we want to update the state based on the
383         # previous state, so we have to loop:
384         for case in self.browse(cr, uid, ids, context=context):
385             values = dict(vals)
386             if case.state in CRM_LEAD_PENDING_STATES:
387                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
388             res = self.write(cr, uid, [case.id], values, context=context)
389
390         return res
391
392     def msg_send(self, cr, uid, id, *args, **argv):
393
394         """ Send The Message
395             @param self: The object pointer
396             @param cr: the current row, from the database cursor,
397             @param uid: the current user’s ID for security checks,
398             @param ids: List of email’s IDs
399             @param *args: Return Tuple Value
400             @param **args: Return Dictionary of Keyword Value
401         """
402         return True
403 crm_lead()
404
405 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: