[IMP]: crm: Put warning on lead creation for stage in comment
[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 import mx.DateTime
27 from tools.translate import _
28 from crm import crm_case
29 import binascii
30 import tools
31
32
33 CRM_LEAD_PENDING_STATES = (
34     crm.AVAILABLE_STATES[2][0], # Cancelled
35     crm.AVAILABLE_STATES[3][0], # Done
36     crm.AVAILABLE_STATES[4][0], # Pending
37 )
38
39 class crm_lead(osv.osv, crm_case):
40     """ CRM Lead Case """
41     _name = "crm.lead"
42     _description = "Lead"
43     _order = "priority, id desc"
44     _inherit = ['mailgate.thread','res.partner.address']
45     def _compute_day(self, cr, uid, ids, fields, args, context={}):
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):
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                             mx.DateTime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
88                             duration,
89                             resource=resource_id
90                         )
91                         no_days = []
92                         date_until = mx.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     _columns = {
103         # Overridden from res.partner.address:
104         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', 
105             select=True, help="Optional linked partner, usually after conversion of the lead"),
106         
107         # From crm.case
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('Watchers Emails', 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', 'Lead Source', \
122                         domain="[('section_id','=',section_id),\
123                         ('object_id.model', '=', 'crm.lead')]"),
124         'type_id': fields.many2one('crm.case.resource.type', 'Lead Type', \
125                          domain="[('section_id','=',section_id),\
126                         ('object_id.model', '=', 'crm.lead')]"),
127         'partner_name': fields.char("Partner Name", size=64),
128         'optin': fields.boolean('Opt-In'),
129         'optout': fields.boolean('Opt-Out'),
130         'type':fields.selection([
131             ('lead','Lead'),
132             ('opportunity','Opportunity'),
133
134         ],'Type', help="Type is used to separate Leads and Opportunities"),
135         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
136         'date_closed': fields.datetime('Closed', readonly=True),
137         'stage_id': fields.many2one('crm.case.stage', 'Stage'),
138         'user_id': fields.many2one('res.users', 'Salesman',help='By Default Salesman is Administrator when create New User'),
139         'referred': fields.char('Referred By', size=64),
140         'date_open': fields.datetime('Opened', readonly=True),
141         'day_open': fields.function(_compute_day, string='Days to Open', \
142                                 method=True, multi='day_open', type="float", store=True),
143         'day_close': fields.function(_compute_day, string='Days to Close', \
144                                 method=True, multi='day_close', type="float", store=True),
145         'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
146                                   help='The state is set to \'Draft\', when a case is created.\
147                                   \nIf the case is in progress the state is set to \'Open\'.\
148                                   \nWhen the case is over, the state is set to \'Done\'.\
149                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'), 
150         'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)], readonly=True),
151         'partner_assigned_id': fields.many2one('res.partner', 'Assigned Partner', help="Partner this case has been forwarded/assigned to.", select=True),
152         'date_assign': fields.date('Assignation Date', help="Last date this case was forwarded/assigned to a partner"),
153     }
154
155     _defaults = {
156         'active': lambda *a: 1,
157         'user_id': crm_case._get_default_user,
158         'email_from': crm_case._get_default_email,
159         'state': lambda *a: 'draft',
160         'type': lambda *a: 'lead',
161         'section_id': crm_case._get_section,
162         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
163         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
164     }
165
166     def create(self, cr, uid, vals, context=None):
167 #       FIXME: Do we really need this? if yes, this is not a correct way to check for stage
168 #        if not vals.get('stage_id',False):
169 #            raise osv.except_osv('Error', _('There is no stage defined for this Sales Team'))
170         return super(crm_lead, self).create(cr, uid, vals, context=context)
171     
172     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
173         """This function returns value of partner email based on Partner Address
174         @param self: The object pointer
175         @param cr: the current row, from the database cursor,
176         @param uid: the current user’s ID for security checks,
177         @param ids: List of case IDs
178         @param add: Id of Partner's address
179         @email: Partner's email ID
180         """
181         if not add:
182             return {'value': {'email_from': False, 'country_id': False}}
183         address = self.pool.get('res.partner.address').browse(cr, uid, add)
184         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
185     
186     def case_open(self, cr, uid, ids, *args):
187         """Overrides cancel for crm_case for setting Open Date
188         @param self: The object pointer
189         @param cr: the current row, from the database cursor,
190         @param uid: the current user’s ID for security checks,
191         @param ids: List of case's Ids
192         @param *args: Give Tuple Value
193         """
194         old_state = self.read(cr, uid, ids, ['state'])[0]['state']
195         res = super(crm_lead, self).case_open(cr, uid, ids, *args)
196         if old_state == 'draft':
197             stage_id = super(crm_lead, self).stage_next(cr, uid, ids, *args)
198             if not stage_id:
199                 raise osv.except_osv(_('Warning !'), _('There is no stage defined for this Sale Team.'))
200             value = self.onchange_stage_id(cr, uid, ids, stage_id, context={})['value']
201             value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'stage_id': stage_id})
202             self.write(cr, uid, ids, value)
203
204         for (id, name) in self.name_get(cr, uid, ids):
205             type = self.browse(cr, uid, id).type or 'Lead'
206             message = (_('The ') + type.title()) + " '" + name + "' "+ _("has been Opened.")
207             self.log(cr, uid, id, message)
208         return res
209
210     def case_close(self, cr, uid, ids, *args):
211         """Overrides close for crm_case for setting close date
212         @param self: The object pointer
213         @param cr: the current row, from the database cursor,
214         @param uid: the current user’s ID for security checks,
215         @param ids: List of case Ids
216         @param *args: Tuple Value for additional Params
217         """
218         res = super(crm_lead, self).case_close(cr, uid, ids, args)
219         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
220         for (id, name) in self.name_get(cr, uid, ids):
221             lead = self.browse(cr, uid, id)
222             if lead.type == 'lead':
223                 message = _('The Lead') + " '" + name + "' "+ _("has been Closed.")
224                 self.log(cr, uid, id, message)
225         return res
226
227     def convert_opportunity(self, cr, uid, ids, context=None):
228         """ Precomputation for converting lead to opportunity
229         @param cr: the current row, from the database cursor,
230         @param uid: the current user’s ID for security checks,
231         @param ids: List of closeday’s IDs
232         @param context: A standard dictionary for contextual values
233         @return: Value of action in dict
234         """
235         if not context:
236             context = {}
237         context.update({'active_ids': ids})
238
239         data_obj = self.pool.get('ir.model.data')
240         data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_action')
241         value = {}
242
243         view_id = False
244         if data_id:
245             view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
246
247         for case in self.browse(cr, uid, ids):
248             context.update({'active_id': case.id})
249             if not case.partner_id:
250                 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
251                 view_id1 = False
252                 if data_id:
253                     view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
254                 value = {
255                         'name': _('Create Partner'),
256                         'view_type': 'form',
257                         'view_mode': 'form,tree',
258                         'res_model': 'crm.lead2opportunity.partner',
259                         'view_id': False,
260                         'context': context,
261                         'views': [(view_id1, 'form')],
262                         'type': 'ir.actions.act_window',
263                         'target': 'new',
264                         'nodestroy': True
265                         }
266                 break
267             else:
268                 value = {
269                         'name': _('Create Opportunity'),
270                         'view_type': 'form',
271                         'view_mode': 'form,tree',
272                         'res_model': 'crm.lead2opportunity.action',
273                         'view_id': False,
274                         'context': context,
275                         'views': [(view_id, 'form')],
276                         'type': 'ir.actions.act_window',
277                         'target': 'new',
278                         'nodestroy': True
279                         }
280         return value
281
282     def stage_next(self, cr, uid, ids, context=None):
283         stage = super(crm_lead, self).stage_next(cr, uid, ids, context)
284         if stage:
285             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
286             if stage_obj.on_change:
287                 data = {'probability': stage_obj.probability}
288                 self.write(cr, uid, ids, data)
289         return stage
290
291     def message_new(self, cr, uid, msg, context):
292         """
293         Automatically calls when new email message arrives
294
295         @param self: The object pointer
296         @param cr: the current row, from the database cursor,
297         @param uid: the current user’s ID for security checks
298         """
299
300         mailgate_pool = self.pool.get('email.server.tools')
301
302         subject = msg.get('subject')
303         body = msg.get('body')
304         msg_from = msg.get('from')
305         priority = msg.get('priority')
306
307         vals = {
308             'name': subject,
309             'email_from': msg_from,
310             'email_cc': msg.get('cc'),
311             'description': body,
312             'user_id': False,
313         }
314         if msg.get('priority', False):
315             vals['priority'] = priority
316
317         res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
318         if res:
319             vals.update(res)
320
321         res = self.create(cr, uid, vals, context)
322         
323         message = _('A Lead created') + " '" + subject + "' " + _("from Mailgate.")
324         self.log(cr, uid, res, message)
325         
326         attachents = msg.get('attachments', [])
327         for attactment in attachents or []:
328             data_attach = {
329                 'name': attactment,
330                 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
331                 'datas_fname': attactment,
332                 'description': 'Mail attachment',
333                 'res_model': self._name,
334                 'res_id': res,
335             }
336             self.pool.get('ir.attachment').create(cr, uid, data_attach)
337
338         return res
339
340     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context={}):
341         """
342         @param self: The object pointer
343         @param cr: the current row, from the database cursor,
344         @param uid: the current user’s ID for security checks,
345         @param ids: List of update mail’s IDs 
346         """
347
348         if isinstance(ids, (str, int, long)):
349             ids = [ids]
350
351         if msg.get('priority'):
352             vals['priority'] = msg.get('priority')
353
354         maps = {
355             'cost':'planned_cost',
356             'revenue': 'planned_revenue',
357             'probability':'probability'
358         }
359         vls = {}
360         for line in msg['body'].split('\n'):
361             line = line.strip()
362             res = tools.misc.command_re.match(line)
363             if res and maps.get(res.group(1).lower()):
364                 key = maps.get(res.group(1).lower())
365                 vls[key] = res.group(2).lower()
366         vals.update(vls)
367
368         # Unfortunately the API is based on lists
369         # but we want to update the state based on the
370         # previous state, so we have to loop:
371         for case in self.browse(cr, uid, ids, context=context):
372             values = dict(vals)
373             if case.state in CRM_LEAD_PENDING_STATES:
374                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
375             res = self.write(cr, uid, [case.id], values, context=context)
376
377         return res
378
379     def msg_send(self, cr, uid, id, *args, **argv):
380
381         """ Send The Message
382             @param self: The object pointer
383             @param cr: the current row, from the database cursor,
384             @param uid: the current user’s ID for security checks,
385             @param ids: List of email’s IDs
386             @param *args: Return Tuple Value
387             @param **args: Return Dictionary of Keyword Value
388         """
389         return True
390 crm_lead()
391
392 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: