1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from osv import fields, osv
23 from datetime import datetime
26 from tools.translate import _
27 from crm import crm_case
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
38 class crm_lead(crm_case, osv.osv):
41 _description = "Lead/Opportunity"
42 _order = "priority,date_action,id desc"
43 _inherit = ['mailgate.thread','res.partner.address']
45 def _compute_day(self, cr, uid, ids, fields, args, context=None):
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
53 cal_obj = self.pool.get('resource.calendar')
54 res_obj = self.pool.get('resource.resource')
57 for lead in self.browse(cr, uid, ids, context=context):
62 if field == 'day_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':
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
77 resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
79 resource_id = resource_ids[0]
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,
86 lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
87 datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
92 date_until = 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:
98 duration = len(no_days)
99 res[lead.id][field] = abs(int(duration))
102 def _history_search(self, cr, uid, obj, name, args, context=None):
104 msg_obj = self.pool.get('mailgate.message')
105 message_ids = msg_obj.search(cr, uid, [('history','=',True), ('name', args[0][1], args[0][2])], context=context)
106 lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
109 return [('id', 'in', lead_ids)]
111 return [('id', '=', '0')]
113 def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
115 for obj in self.browse(cr, uid, ids, context=context):
117 for msg in obj.message_ids:
119 res[obj.id] = msg.name
124 # Overridden from res.partner.address:
125 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
126 select=True, help="Optional linked partner, usually after conversion of the lead"),
128 'id': fields.integer('ID'),
129 'name': fields.char('Name', size=64, select=1),
130 'active': fields.boolean('Active', required=False),
131 'date_action_last': fields.datetime('Last Action', readonly=1),
132 'date_action_next': fields.datetime('Next Action', readonly=1),
133 'email_from': fields.char('Email', size=128, help="E-mail address of the contact", select=1),
134 'section_id': fields.many2one('crm.case.section', 'Sales Team', \
135 select=True, help='When sending mails, the default email address is taken from the sales team.'),
136 'create_date': fields.datetime('Creation Date' , readonly=True),
137 '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"),
138 'description': fields.text('Notes'),
139 'write_date': fields.datetime('Update Date' , readonly=True),
141 'categ_id': fields.many2one('crm.case.categ', 'Category', \
142 domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
143 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
144 domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
145 'channel_id': fields.many2one('res.partner.canal', 'Channel', help="From which channel (mail, direct, phone, ...) did this contact reach you?"),
146 'contact_name': fields.char('Contact Name', size=64),
147 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the into opportunity', select=1),
148 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
149 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
150 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
151 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
152 'date_closed': fields.datetime('Closed', readonly=True),
153 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[(section_ids', '=', section_id)]"),
154 'user_id': fields.many2one('res.users', 'Salesman'),
155 'referred': fields.char('Referred By', size=64),
156 'date_open': fields.datetime('Opened', readonly=True),
157 'day_open': fields.function(_compute_day, string='Days to Open', \
158 multi='day_open', type="float", store=True),
159 'day_close': fields.function(_compute_day, string='Days to Close', \
160 multi='day_close', type="float", store=True),
161 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
162 help='The state is set to \'Draft\', when a case is created.\
163 \nIf the case is in progress the state is set to \'Open\'.\
164 \nWhen the case is over, the state is set to \'Done\'.\
165 \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
166 'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
167 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
170 # Only used for type opportunity
171 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
172 'probability': fields.float('Probability (%)',group_operator="avg"),
173 'planned_revenue': fields.float('Expected Revenue'),
174 'ref': fields.reference('Reference', selection=crm._links_get, size=128),
175 'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
176 'phone': fields.char("Phone", size=64),
177 'date_deadline': fields.date('Expected Closing'),
178 'date_action': fields.date('Next Action Date'),
179 'title_action': fields.char('Next Action', size=64),
180 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[(section_ids', '=', section_id)]"),
184 'active': lambda *a: 1,
185 'user_id': crm_case._get_default_user,
186 'email_from': crm_case._get_default_email,
187 'state': lambda *a: 'draft',
188 'type': lambda *a: 'lead',
189 'section_id': crm_case._get_section,
190 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
191 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
192 #'stage_id': _get_stage_id,
195 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
196 """This function returns value of partner email based on Partner Address
199 return {'value': {'email_from': False, 'country_id': False}}
200 address = self.pool.get('res.partner.address').browse(cr, uid, add)
201 return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
203 def on_change_optin(self, cr, uid, ids, optin):
204 return {'value':{'optin':optin,'optout':False}}
206 def on_change_optout(self, cr, uid, ids, optout):
207 return {'value':{'optout':optout,'optin':False}}
209 def onchange_stage_id(self, cr, uid, ids, stage_id, context={}):
212 stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
213 if not stage.on_change:
215 return {'value':{'probability': stage.probability}}
217 def stage_find_percent(self, cr, uid, percent, section_id):
218 """ Return the first stage with a probability == percent
220 stage_pool = self.pool.get('crm.case.stage')
222 ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])])
224 ids = stage_pool.search(cr, uid, [("probability", '=', percent)])
230 def stage_find_lost(self, cr, uid, section_id):
231 return self.stage_find_percent(cr, uid, 0.0, section_id)
233 def stage_find_won(self, cr, uid, section_id):
234 return self.stage_find_percent(cr, uid, 100.0, section_id)
236 def case_open(self, cr, uid, ids, *args):
237 for l in self.browse(cr, uid, ids):
238 # When coming from draft override date and stage otherwise just set state
239 if l.state == 'draft':
241 message = _("The lead '%s' has been opened.") % l.name
242 elif l.type == 'opportunity':
243 message = _("The opportunity '%s' has been opened.") % l.name
245 message = _("The case '%s' has been opened.") % l.name
246 self.log(cr, uid, l.id, message)
247 value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}
248 self.write(cr, uid, [l.id], value)
249 if l.type == 'opportunity' and not l.stage_id:
250 stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)])
252 self.stage_set(cr, uid, [l.id], stage_id)
253 res = super(crm_lead, self).case_open(cr, uid, ids, *args)
256 def case_close(self, cr, uid, ids, *args):
257 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
258 self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
259 for case in self.browse(cr, uid, ids):
260 if case.type == 'lead':
261 message = _("The lead '%s' has been closed.") % case.name
263 message = _("The case '%s' has been closed.") % case.name
264 self.log(cr, uid, case.id, message)
267 def case_cancel(self, cr, uid, ids, *args):
268 """Overrides cancel for crm_case for setting probability
270 res = super(crm_lead, self).case_cancel(cr, uid, ids, args)
271 self.write(cr, uid, ids, {'probability' : 0.0})
274 def case_reset(self, cr, uid, ids, *args):
275 """Overrides reset as draft in order to set the stage field as empty
277 res = super(crm_lead, self).case_reset(cr, uid, ids, *args)
278 self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0})
281 def case_mark_lost(self, cr, uid, ids, *args):
282 """Mark the case as lost: state = done and probability = 0%
284 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
285 self.write(cr, uid, ids, {'probability' : 0.0})
286 for l in self.browse(cr, uid, ids):
287 stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False)
289 self.stage_set(cr, uid, [l.id], stage_id)
290 message = _("The opportunity '%s' has been marked as lost.") % l.name
291 self.log(cr, uid, l.id, message)
294 def case_mark_won(self, cr, uid, ids, *args):
295 """Mark the case as lost: state = done and probability = 0%
297 res = super(crm_lead, self).case_close(cr, uid, ids, *args)
298 self.write(cr, uid, ids, {'probability' : 100.0})
299 for l in self.browse(cr, uid, ids):
300 stage_id = self.stage_find_won(cr, uid, l.section_id.id or False)
302 self.stage_set(cr, uid, [l.id], stage_id)
303 message = _("The opportunity '%s' has been been won.") % l.name
304 self.log(cr, uid, l.id, message)
307 def convert_opportunity(self, cr, uid, ids, context=None):
308 """ Precomputation for converting lead to opportunity
312 context.update({'active_ids': ids})
314 data_obj = self.pool.get('ir.model.data')
318 for case in self.browse(cr, uid, ids, context=context):
319 context.update({'active_id': case.id})
320 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
323 view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
325 'name': _('Create Partner'),
327 'view_mode': 'form,tree',
328 'res_model': 'crm.lead2opportunity.partner',
331 'views': [(view_id1, 'form')],
332 'type': 'ir.actions.act_window',
338 def message_new(self, cr, uid, msg, context=None):
339 """ Automatically calls when new email message arrives
341 mailgate_pool = self.pool.get('email.server.tools')
343 subject = msg.get('subject') or _("No Subject")
344 body = msg.get('body')
345 msg_from = msg.get('from')
346 priority = msg.get('priority')
350 'email_from': msg_from,
351 'email_cc': msg.get('cc'),
355 if msg.get('priority', False):
356 vals['priority'] = priority
358 res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
362 res = self.create(cr, uid, vals, context)
363 attachents = msg.get('attachments', [])
364 for attactment in attachents or []:
367 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
368 'datas_fname': attactment,
369 'description': 'Mail attachment',
370 'res_model': self._name,
373 self.pool.get('ir.attachment').create(cr, uid, data_attach)
377 def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
379 @param ids: List of update mail’s IDs
381 if isinstance(ids, (str, int, long)):
384 if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
385 vals['priority'] = msg.get('priority')
388 'cost':'planned_cost',
389 'revenue': 'planned_revenue',
390 'probability':'probability'
393 for line in msg['body'].split('\n'):
395 res = tools.misc.command_re.match(line)
396 if res and maps.get(res.group(1).lower()):
397 key = maps.get(res.group(1).lower())
398 vls[key] = res.group(2).lower()
401 # Unfortunately the API is based on lists
402 # but we want to update the state based on the
403 # previous state, so we have to loop:
404 for case in self.browse(cr, uid, ids, context=context):
406 if case.state in CRM_LEAD_PENDING_STATES:
407 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
408 res = self.write(cr, uid, [case.id], values, context=context)
411 def msg_send(self, cr, uid, id, *args, **argv):
413 @param ids: List of email’s IDs
417 def action_makeMeeting(self, cr, uid, ids, context=None):
419 This opens Meeting's calendar view to schedule meeting on current Opportunity
420 @return : Dictionary value for created Meeting view
423 for opp in self.browse(cr, uid, ids, context=context):
424 data_obj = self.pool.get('ir.model.data')
427 result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
428 res = data_obj.read(cr, uid, result, ['res_id'])
429 id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
430 id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
431 id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
433 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
435 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
437 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
440 'default_opportunity_id': opp.id,
441 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
442 'default_user_id': uid,
443 'default_section_id': opp.section_id and opp.section_id.id or False,
444 'default_email_from': opp.email_from,
445 'default_state': 'open',
446 'default_name': opp.name
449 'name': _('Meetings'),
452 'view_mode': 'calendar,form,tree',
453 'res_model': 'crm.meeting',
455 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
456 'type': 'ir.actions.act_window',
457 'search_view_id': res['res_id'],
462 def write(self, cr, uid, ids, vals, context=None):
466 if 'date_closed' in vals:
467 return super(crm_lead,self).write(cr, uid, ids, vals, context=context)
469 if 'stage_id' in vals and vals['stage_id']:
470 stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
471 self.history(cr, uid, ids, _("Changed Stage to: %s") % stage_obj.name, details=_("Changed Stage to: %s") % stage_obj.name)
473 for case in self.browse(cr, uid, ids, context=context):
474 if case.type == 'lead' or context.get('stage_type',False)=='lead':
475 message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
476 elif case.type == 'opportunity':
477 message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name)
478 self.log(cr, uid, case.id, message)
479 return super(crm_lead,self).write(cr, uid, ids, vals, context)
481 def unlink(self, cr, uid, ids, context=None):
482 for lead in self.browse(cr, uid, ids, context):
483 if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'):
484 raise osv.except_osv(_('Warning !'),
485 _('You can not delete this lead. You should better cancel it.'))
486 return super(crm_lead, self).unlink(cr, uid, ids, context)
491 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: