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