[IMP] crm, leads and opportunities: when opening a case, the stage shouldn't change...
[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.project.bug')]"),
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("Partner Name", size=64),
129         'optin': fields.boolean('Opt-In'),
130         'optout': fields.boolean('Opt-Out'),
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({'stage_id': stage_id})
196                     value.update(self.onchange_stage_id(cr, uid, ids, stage_id, context={})['value'])
197             value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
198             self.write(cr, uid, ids, value)
199
200         for (id, name) in self.name_get(cr, uid, ids):
201             type = self.browse(cr, uid, id).type or 'Lead'
202             message = (_('The ') + type.title()) + " '" + name + "' "+ _("has been Opened.")
203             self.log(cr, uid, id, message)
204         return res
205
206     def case_close(self, cr, uid, ids, *args):
207         """Overrides close for crm_case for setting close date
208         @param self: The object pointer
209         @param cr: the current row, from the database cursor,
210         @param uid: the current user’s ID for security checks,
211         @param ids: List of case Ids
212         @param *args: Tuple Value for additional Params
213         """
214         res = super(crm_lead, self).case_close(cr, uid, ids, args)
215         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
216         for (id, name) in self.name_get(cr, uid, ids):
217             lead = self.browse(cr, uid, id)
218             if lead.type == 'lead':
219                 message = _('The Lead') + " '" + name + "' "+ _("has been Closed.")
220                 self.log(cr, uid, id, message)
221         return res
222
223     def convert_opportunity(self, cr, uid, ids, context=None):
224         """ Precomputation for converting lead to opportunity
225         @param cr: the current row, from the database cursor,
226         @param uid: the current user’s ID for security checks,
227         @param ids: List of closeday’s IDs
228         @param context: A standard dictionary for contextual values
229         @return: Value of action in dict
230         """
231         if not context:
232             context = {}
233         context.update({'active_ids': ids})
234
235         data_obj = self.pool.get('ir.model.data')
236         data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_action')
237         value = {}
238
239         view_id = False
240         if data_id:
241             view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
242
243         for case in self.browse(cr, uid, ids):
244             context.update({'active_id': case.id})
245             if not case.partner_id:
246                 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
247                 view_id1 = False
248                 if data_id:
249                     view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
250                 value = {
251                         'name': _('Create Partner'),
252                         'view_type': 'form',
253                         'view_mode': 'form,tree',
254                         'res_model': 'crm.lead2opportunity.partner',
255                         'view_id': False,
256                         'context': context,
257                         'views': [(view_id1, 'form')],
258                         'type': 'ir.actions.act_window',
259                         'target': 'new',
260                         'nodestroy': True
261                         }
262                 break
263             else:
264                 value = {
265                         'name': _('Create Opportunity'),
266                         'view_type': 'form',
267                         'view_mode': 'form,tree',
268                         'res_model': 'crm.lead2opportunity.action',
269                         'view_id': False,
270                         'context': context,
271                         'views': [(view_id, 'form')],
272                         'type': 'ir.actions.act_window',
273                         'target': 'new',
274                         'nodestroy': True
275                         }
276         return value
277
278     def stage_next(self, cr, uid, ids, context=None):
279         stage = super(crm_lead, self).stage_next(cr, uid, ids, context)
280         if stage:
281             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
282             if stage_obj.on_change:
283                 data = {'probability': stage_obj.probability}
284                 self.write(cr, uid, ids, data)
285         return stage
286
287     def message_new(self, cr, uid, msg, context):
288         """
289         Automatically calls when new email message arrives
290
291         @param self: The object pointer
292         @param cr: the current row, from the database cursor,
293         @param uid: the current user’s ID for security checks
294         """
295
296         mailgate_pool = self.pool.get('email.server.tools')
297
298         subject = msg.get('subject')
299         body = msg.get('body')
300         msg_from = msg.get('from')
301         priority = msg.get('priority')
302
303         vals = {
304             'name': subject,
305             'email_from': msg_from,
306             'email_cc': msg.get('cc'),
307             'description': body,
308             'user_id': False,
309         }
310         if msg.get('priority', False):
311             vals['priority'] = priority
312
313         res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
314         if res:
315             vals.update(res)
316
317         res = self.create(cr, uid, vals, context)
318         
319         message = _('A Lead created') + " '" + subject + "' " + _("from Mailgate.")
320         self.log(cr, uid, res, message)
321         
322         attachents = msg.get('attachments', [])
323         for attactment in attachents or []:
324             data_attach = {
325                 'name': attactment,
326                 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
327                 'datas_fname': attactment,
328                 'description': 'Mail attachment',
329                 'res_model': self._name,
330                 'res_id': res,
331             }
332             self.pool.get('ir.attachment').create(cr, uid, data_attach)
333
334         return res
335
336     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context={}):
337         """
338         @param self: The object pointer
339         @param cr: the current row, from the database cursor,
340         @param uid: the current user’s ID for security checks,
341         @param ids: List of update mail’s IDs 
342         """
343
344         if isinstance(ids, (str, int, long)):
345             ids = [ids]
346
347         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
348             vals['priority'] = msg.get('priority')
349
350         maps = {
351             'cost':'planned_cost',
352             'revenue': 'planned_revenue',
353             'probability':'probability'
354         }
355         vls = {}
356         for line in msg['body'].split('\n'):
357             line = line.strip()
358             res = tools.misc.command_re.match(line)
359             if res and maps.get(res.group(1).lower()):
360                 key = maps.get(res.group(1).lower())
361                 vls[key] = res.group(2).lower()
362         vals.update(vls)
363
364         # Unfortunately the API is based on lists
365         # but we want to update the state based on the
366         # previous state, so we have to loop:
367         for case in self.browse(cr, uid, ids, context=context):
368             values = dict(vals)
369             if case.state in CRM_LEAD_PENDING_STATES:
370                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
371             res = self.write(cr, uid, [case.id], values, context=context)
372
373         return res
374
375     def msg_send(self, cr, uid, id, *args, **argv):
376
377         """ Send The Message
378             @param self: The object pointer
379             @param cr: the current row, from the database cursor,
380             @param uid: the current user’s ID for security checks,
381             @param ids: List of email’s IDs
382             @param *args: Return Tuple Value
383             @param **args: Return Dictionary of Keyword Value
384         """
385         return True
386 crm_lead()
387
388 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: