[IMP] sale order line invisible type
[odoo/odoo.git] / addons / crm / crm_lead.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
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 import binascii
23 from base_status.base_stage import base_stage
24 import crm
25 from datetime import datetime
26 from mail.mail_message import to_email
27 from osv import fields, osv
28 import time
29 import tools
30 from tools.translate import _
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(base_stage, osv.osv):
39     """ CRM Lead Case """
40     _name = "crm.lead"
41     _description = "Lead/Opportunity"
42     _order = "priority,date_action,id desc"
43     _inherit = ['ir.needaction_mixin', 'mail.thread']
44     _mail_compose_message = True
45
46     def _get_default_section_id(self, cr, uid, context=None):
47         """ Gives default section by checking if present in the context """
48         return (self._resolve_section_id_from_context(cr, uid, context=context) or False)
49
50     def _get_default_stage_id(self, cr, uid, context=None):
51         """ Gives default stage_id """
52         section_id = self._get_default_section_id(cr, uid, context=context)
53         return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'), ('type', '=', 'both')], context=context)
54
55     def _resolve_section_id_from_context(self, cr, uid, context=None):
56         """ Returns ID of section based on the value of 'section_id'
57             context key, or None if it cannot be resolved to a single
58             Sales Team.
59         """
60         if context is None:
61             context = {}
62         if type(context.get('default_section_id')) in (int, long):
63             return context.get('default_section_id')
64         if isinstance(context.get('default_section_id'), basestring):
65             section_name = context['default_section_id']
66             section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
67             if len(section_ids) == 1:
68                 return int(section_ids[0][0])
69         return None
70
71     def _resolve_type_from_context(self, cr, uid, context=None):
72         """ Returns the type (lead or opportunity) from the type context
73             key. Returns None if it cannot be resolved.
74         """
75         if context is None:
76             context = {}
77         return context.get('default_type')
78
79     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
80         access_rights_uid = access_rights_uid or uid
81         stage_obj = self.pool.get('crm.case.stage')
82         order = stage_obj._order
83         # lame hack to allow reverting search, should just work in the trivial case
84         if read_group_order == 'stage_id desc':
85             order = "%s desc" % order
86         # retrieve section_id from the context and write the domain
87         # - ('id', 'in', 'ids'): add columns that should be present
88         # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
89         # - OR ('section_ids', '=', section_id), ('fold', '=', False) if section_id: add section columns that are not folded
90         search_domain = []
91         section_id = self._resolve_section_id_from_context(cr, uid, context=context)
92         if section_id:
93             search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', False)]
94         search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
95         # retrieve type from the context (if set: choose 'type' or 'both')
96         type = self._resolve_type_from_context(cr, uid, context=context)
97         if type:
98             search_domain += ['|', ('type', '=', type), ('type', '=', 'both')]
99         # perform search
100         stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
101         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
102         # restore order of the search
103         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
104         return result
105
106     _group_by_full = {
107         'stage_id': _read_group_stage_ids
108     }
109
110     def _compute_day(self, cr, uid, ids, fields, args, context=None):
111         """
112         @param cr: the current row, from the database cursor,
113         @param uid: the current user’s ID for security checks,
114         @param ids: List of Openday’s IDs
115         @return: difference between current date and log date
116         @param context: A standard dictionary for contextual values
117         """
118         cal_obj = self.pool.get('resource.calendar')
119         res_obj = self.pool.get('resource.resource')
120
121         res = {}
122         for lead in self.browse(cr, uid, ids, context=context):
123             for field in fields:
124                 res[lead.id] = {}
125                 duration = 0
126                 ans = False
127                 if field == 'day_open':
128                     if lead.date_open:
129                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
130                         date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
131                         ans = date_open - date_create
132                         date_until = lead.date_open
133                 elif field == 'day_close':
134                     if lead.date_closed:
135                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
136                         date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
137                         date_until = lead.date_closed
138                         ans = date_close - date_create
139                 if ans:
140                     resource_id = False
141                     if lead.user_id:
142                         resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
143                         if len(resource_ids):
144                             resource_id = resource_ids[0]
145
146                     duration = float(ans.days)
147                     if lead.section_id and lead.section_id.resource_calendar_id:
148                         duration =  float(ans.days) * 24
149                         new_dates = cal_obj.interval_get(cr,
150                             uid,
151                             lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
152                             datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
153                             duration,
154                             resource=resource_id
155                         )
156                         no_days = []
157                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
158                         for in_time, out_time in new_dates:
159                             if in_time.date not in no_days:
160                                 no_days.append(in_time.date)
161                             if out_time > date_until:
162                                 break
163                         duration =  len(no_days)
164                 res[lead.id][field] = abs(int(duration))
165         return res
166
167     def _history_search(self, cr, uid, obj, name, args, context=None):
168         res = []
169         msg_obj = self.pool.get('mail.message')
170         message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
171         lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
172
173         if lead_ids:
174             return [('id', 'in', lead_ids)]
175         else:
176             return [('id', '=', '0')]
177
178     def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
179         res = {}
180         for obj in self.browse(cr, uid, ids, context=context):
181             res[obj.id] = ''
182             for msg in obj.message_ids:
183                 if msg.email_from:
184                     res[obj.id] = msg.subject
185                     break
186         return res
187
188     _columns = {
189         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
190             select=True, help="Optional linked partner, usually after conversion of the lead"),
191
192         'id': fields.integer('ID', readonly=True),
193         'name': fields.char('Subject', size=64, required=True, select=1),
194         'active': fields.boolean('Active', required=False),
195         'date_action_last': fields.datetime('Last Action', readonly=1),
196         'date_action_next': fields.datetime('Next Action', readonly=1),
197         'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
198         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
199                         select=True, help='When sending mails, the default email address is taken from the sales team.'),
200         'create_date': fields.datetime('Creation Date' , readonly=True),
201         '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"),
202         'description': fields.text('Notes'),
203         'write_date': fields.datetime('Update Date' , readonly=True),
204         'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
205             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
206         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
207             domain="['|',('section_id','=',section_id),('section_id','=',False)]", help="From which campaign (seminar, marketing campaign, mass mailing, ...) did this contact come from?"),
208         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
209         'contact_name': fields.char('Contact Name', size=64),
210         'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
211         'opt_out': fields.boolean('Opt-Out', oldname='optout', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
212         'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
213         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
214         'date_closed': fields.datetime('Closed', readonly=True),
215         'stage_id': fields.many2one('crm.case.stage', 'Stage',
216                         domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
217         'user_id': fields.many2one('res.users', 'Salesperson'),
218         'referred': fields.char('Referred By', size=64),
219         'date_open': fields.datetime('Opened', readonly=True),
220         'day_open': fields.function(_compute_day, string='Days to Open', \
221                                 multi='day_open', type="float", store=True),
222         'day_close': fields.function(_compute_day, string='Days to Close', \
223                                 multi='day_close', type="float", store=True),
224         'state': fields.related('stage_id', 'state', type="selection", store=True,
225                 selection=crm.AVAILABLE_STATES, string="State", readonly=True,
226                 help='The state is set to \'Draft\', when a case is created.\
227                       If the case is in progress the state is set to \'Open\'.\
228                       When the case is over, the state is set to \'Done\'.\
229                       If the case needs to be reviewed then the state is \
230                       set to \'Pending\'.'),
231         'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
232
233         # Only used for type opportunity
234         'probability': fields.float('Success Rate (%)',group_operator="avg"),
235         'planned_revenue': fields.float('Expected Revenue'),
236         'ref': fields.reference('Reference', selection=crm._links_get, size=128),
237         'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
238         'phone': fields.char("Phone", size=64),
239         'date_deadline': fields.date('Expected Closing'),
240         'date_action': fields.date('Next Action Date', select=True),
241         'title_action': fields.char('Next Action', size=64),
242         'color': fields.integer('Color Index'),
243         'partner_address_name': fields.related('partner_id', 'name', type='char', string='Partner Contact Name', readonly=True),
244         'partner_address_email': fields.related('partner_id', 'email', type='char', string='Partner Contact Email', readonly=True),
245         'company_currency': fields.related('company_id', 'currency_id', 'symbol', type='char', string='Company Currency', readonly=True),
246         'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
247         'user_login': fields.related('user_id', 'login', type='char', string='User Login', readonly=True),
248
249         # Fields for address, due to separation from crm and res.partner
250         'street': fields.char('Street', size=128),
251         'street2': fields.char('Street2', size=128),
252         'zip': fields.char('Zip', change_default=True, size=24),
253         'city': fields.char('City', size=128),
254         'state_id': fields.many2one("res.country.state", 'State', domain="[('country_id','=',country_id)]"),
255         'country_id': fields.many2one('res.country', 'Country'),
256         'phone': fields.char('Phone', size=64),
257         'fax': fields.char('Fax', size=64),
258         'mobile': fields.char('Mobile', size=64),
259         'function': fields.char('Function', size=128),
260         'title': fields.many2one('res.partner.title', 'Title'),
261         'company_id': fields.many2one('res.company', 'Company', select=1),
262         'payment_mode': fields.many2one('crm.payment.mode', 'Payment Mode', \
263                             domain="[('section_id','=',section_id)]"),
264         'planned_cost': fields.float('Planned Costs'),
265     }
266
267     _defaults = {
268         'active': 1,
269         'type': 'lead',
270         'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
271         'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
272         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
273         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
274         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
275         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
276         'color': 0,
277     }
278
279     def create(self, cr, uid, vals, context=None):
280         obj_id = super(crm_lead, self).create(cr, uid, vals, context)
281         self.create_send_note(cr, uid, [obj_id], context=context)
282         return obj_id
283
284     def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
285         if not stage_id:
286             return {'value':{}}
287         stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
288         if not stage.on_change:
289             return {'value':{}}
290         return {'value':{'probability': stage.probability}}
291
292     def on_change_partner(self, cr, uid, ids, partner_id, context=None):
293         result = {}
294         values = {}
295         if partner_id:
296             partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
297             values = {
298                 'partner_name' : partner.name, 
299                 'street' : partner.street,
300                 'street2' : partner.street2,
301                 'city' : partner.city,
302                 'state_id' : partner.state_id and partner.state_id.id or False,
303                 'country_id' : partner.country_id and partner.country_id.id or False,
304             }
305         return {'value' : values}
306
307
308     def _check(self, cr, uid, ids=False, context=None):
309         """ Override of the base.stage method.
310             Function called by the scheduler to process cases for date actions
311             Only works on not done and cancelled cases
312         """
313         cr.execute('select * from crm_case \
314                 where (date_action_last<%s or date_action_last is null) \
315                 and (date_action_next<=%s or date_action_next is null) \
316                 and state not in (\'cancel\',\'done\')',
317                 (time.strftime("%Y-%m-%d %H:%M:%S"),
318                     time.strftime('%Y-%m-%d %H:%M:%S')))
319
320         ids2 = map(lambda x: x[0], cr.fetchall() or [])
321         cases = self.browse(cr, uid, ids2, context=context)
322         return self._action(cr, uid, cases, False, context=context)
323
324     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
325         """ Override of the base.stage method
326             Parameter of the stage search taken from the lead:
327             - type: stage type must be the same or 'both'
328             - section_id: if set, stages must belong to this section or
329               be a default stage; if not set, stages must be default
330               stages
331         """
332         if isinstance(cases, (int, long)):
333             cases = self.browse(cr, uid, cases, context=context)
334         # collect all section_ids
335         section_ids = []
336         types = ['both']
337         if section_id:
338             section_ids.append(section_id)
339         for lead in cases:
340             if lead.section_id:
341                 section_ids.append(lead.section_id.id)
342             if lead.type not in types:
343                 types.append(lead.type)
344         # OR all section_ids and OR with case_default
345         search_domain = []
346         if section_ids:
347             search_domain += [('|')] * len(section_ids)
348             for section_id in section_ids:
349                 search_domain.append(('section_ids', '=', section_id))
350         search_domain.append(('case_default', '=', True))
351         # AND with cases types
352         search_domain.append(('type', 'in', types))
353         # AND with the domain in parameter
354         search_domain += list(domain)
355         # perform search, return the first found
356         stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
357         if stage_ids:
358             return stage_ids[0]
359         return False
360
361     def case_cancel(self, cr, uid, ids, context=None):
362         """ Overrides case_cancel from base_stage to set probability """
363         res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
364         self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
365         return res
366
367     def case_reset(self, cr, uid, ids, context=None):
368         """ Overrides case_reset from base_stage to set probability """
369         res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
370         self.write(cr, uid, ids, {'probability': 0.0}, context=context)
371         return res
372
373     def case_mark_lost(self, cr, uid, ids, context=None):
374         """ Mark the case as lost: state=cancel and probability=0 """
375         for lead in self.browse(cr, uid, ids):
376             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context)
377             if stage_id:
378                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
379         self.case_mark_lost_send_note(cr, uid, ids, context=context)
380         return True
381
382     def case_mark_won(self, cr, uid, ids, context=None):
383         """ Mark the case as lost: state=done and probability=100 """
384         for lead in self.browse(cr, uid, ids):
385             stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context)
386             if stage_id:
387                 self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
388         self.case_mark_won_send_note(cr, uid, ids, context=context)
389         return True
390
391     def set_priority(self, cr, uid, ids, priority):
392         """ Set lead priority
393         """
394         return self.write(cr, uid, ids, {'priority' : priority})
395
396     def set_high_priority(self, cr, uid, ids, context=None):
397         """ Set lead priority to high
398         """
399         return self.set_priority(cr, uid, ids, '1')
400
401     def set_normal_priority(self, cr, uid, ids, context=None):
402         """ Set lead priority to normal
403         """
404         return self.set_priority(cr, uid, ids, '3')
405
406     def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
407         # prepare opportunity data into dictionary for merging
408         opportunities = self.browse(cr, uid, ids, context=context)
409         def _get_first_not_null(attr):
410             if hasattr(oldest, attr):
411                 return getattr(oldest, attr)
412             for opportunity in opportunities:
413                 if hasattr(opportunity, attr):
414                     return getattr(opportunity, attr)
415             return False
416
417         def _get_first_not_null_id(attr):
418             res = _get_first_not_null(attr)
419             return res and res.id or False
420
421         def _concat_all(attr):
422             return ', '.join(filter(lambda x: x, [getattr(opportunity, attr) or '' for opportunity in opportunities if hasattr(opportunity, attr)]))
423
424         data = {}
425         for field_name in fields:
426             field_info = self._all_columns.get(field_name)
427             if field_info is None:
428                 continue
429             field = field_info.column
430             if field._type in ('many2many', 'one2many'):
431                 continue
432             elif field._type == 'many2one':
433                 data[field_name] = _get_first_not_null_id(field_name)  # !!
434             elif field._type == 'text':
435                 data[field_name] = _concat_all(field_name)  #not lost
436             else:
437                 data[field_name] = _get_first_not_null(field_name)  #not lost
438         return data
439
440     def _merge_find_oldest(self, cr, uid, ids, context=None):
441         if context is None:
442             context = {}
443         #TOCHECK: where pass 'convert' in context ?
444         if context.get('convert'):
445             ids = list(set(ids) - set(context.get('lead_ids', False)) )
446
447         #search opportunities order by create date
448         opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date' , context=context)
449         oldest_id = opportunity_ids[0]
450         return self.browse(cr, uid, oldest_id, context=context)
451
452     def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
453         body = []
454         if title:
455             body.append("%s\n" % (title))
456         for field_name in fields:
457             field_info = self._all_columns.get(field_name)
458             if field_info is None:
459                 continue
460             field = field_info.column
461             value = None
462
463             if field._type == 'selection':
464                 if hasattr(field.selection, '__call__'):
465                     key = field.selection(self, cr, uid, context=context)
466                 else:
467                     key = field.selection
468                 value = dict(key).get(lead[field_name], lead[field_name])
469             elif field._type == 'many2one':
470                 if lead[field_name]:
471                     value = lead[field_name].name_get()[0][1]
472             else:
473                 value = lead[field_name]
474
475             body.append("%s: %s" % (field.string, value or ''))
476         return "\n".join(body + ['---'])
477
478     def _merge_notification(self, cr, uid, opportunity_id, opportunities, context=None):
479         #TOFIX: mail template should be used instead of fix body, subject text
480         details = []
481         merge_message = _('Merged opportunities')
482         subject = [merge_message]
483         fields = ['name', 'partner_id', 'stage_id', 'section_id', 'user_id', 'categ_ids', 'channel_id', 'company_id', 'contact_name',
484                   'email_from', 'phone', 'fax', 'mobile', 'state_id', 'description', 'probability', 'planned_revenue',
485                   'country_id', 'city', 'street', 'street2', 'zip']
486         for opportunity in opportunities:
487             subject.append(opportunity.name)
488             title = "%s : %s" % (merge_message, opportunity.name)
489             details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
490
491         subject = subject[0] + ", ".join(subject[1:])
492         details = "\n\n".join(details)
493         return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
494
495     def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
496         message = self.pool.get('mail.message')
497         for opportunity in opportunities:
498             for history in opportunity.message_ids:
499                 message.write(cr, uid, history.id, {
500                         'res_id': opportunity_id,
501                         'subject' : _("From %s : %s") % (opportunity.name, history.subject)
502                 }, context=context)
503
504         return True
505
506     def _merge_opportunity_attachments(self, cr, uid, opportunity_id, opportunities, context=None):
507         attachment = self.pool.get('ir.attachment')
508
509         # return attachments of opportunity
510         def _get_attachments(opportunity_id):
511             attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', opportunity_id)], context=context)
512             return attachment.browse(cr, uid, attachment_ids, context=context)
513
514         count = 1
515         first_attachments = _get_attachments(opportunity_id)
516         for opportunity in opportunities:
517             attachments = _get_attachments(opportunity.id)
518             for first in first_attachments:
519                 for attachment in attachments:
520                     if attachment.name == first.name:
521                         values = dict(
522                             name = "%s (%s)" % (attachment.name, count,),
523                             res_id = opportunity_id,
524                         )
525                         attachment.write(values)
526                         count+=1
527
528         return True
529
530     def merge_opportunity(self, cr, uid, ids, context=None):
531         """
532         To merge opportunities
533             :param ids: list of opportunities ids to merge
534         """
535         if context is None: context = {}
536
537         #TOCHECK: where pass lead_ids in context?
538         lead_ids = context and context.get('lead_ids', []) or []
539
540         if len(ids) <= 1:
541             raise osv.except_osv(_('Warning!'),_('Please select more than one opportunity from the list view.'))
542
543         ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
544         opportunities = self.browse(cr, uid, ids, context=context)
545         opportunities_list = list(set(opportunities) - set(ctx_opportunities))
546         oldest = self._merge_find_oldest(cr, uid, ids, context=context)
547         if ctx_opportunities :
548             first_opportunity = ctx_opportunities[0]
549             tail_opportunities = opportunities_list
550         else:
551             first_opportunity = opportunities_list[0]
552             tail_opportunities = opportunities_list[1:]
553
554         fields = ['partner_id', 'title', 'name', 'categ_ids', 'channel_id', 'city', 'company_id', 'contact_name', 'country_id', 'type_id', 'user_id', 'section_id', 'state_id', 'description', 'email', 'fax', 'mobile',
555             'partner_name', 'phone', 'probability', 'planned_revenue', 'street', 'street2', 'zip', 'create_date', 'date_action_last',
556             'date_action_next', 'email_from', 'email_cc', 'partner_name']
557
558         data = self._merge_data(cr, uid, ids, oldest, fields, context=context)
559
560         # merge data into first opportunity
561         self.write(cr, uid, [first_opportunity.id], data, context=context)
562
563         #copy message and attachements into the first opportunity
564         self._merge_opportunity_history(cr, uid, first_opportunity.id, tail_opportunities, context=context)
565         self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
566
567         #Notification about loss of information
568         self._merge_notification(cr, uid, first_opportunity, opportunities, context=context)
569         #delete tail opportunities
570         self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
571
572         #open first opportunity
573         self.case_open(cr, uid, [first_opportunity.id])
574         return first_opportunity.id
575
576     def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
577         crm_stage = self.pool.get('crm.case.stage')
578         contact_id = False
579         if customer:
580             contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
581         if not section_id:
582             section_id = lead.section_id and lead.section_id.id or False
583         if section_id:
584             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1), ('section_ids','=', section_id)])
585         else:
586             stage_ids = crm_stage.search(cr, uid, [('sequence','>=',1)])
587         stage_id = stage_ids and stage_ids[0] or False
588         return {
589                 'planned_revenue': lead.planned_revenue,
590                 'probability': lead.probability,
591                 'name': lead.name,
592                 'partner_id': customer and customer.id or False,
593                 'user_id': (lead.user_id and lead.user_id.id),
594                 'type': 'opportunity',
595                 'stage_id': stage_id or False,
596                 'date_action': time.strftime('%Y-%m-%d %H:%M:%S'),
597                 'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),
598         }
599
600     def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
601         partner = self.pool.get('res.partner')
602         mail_message = self.pool.get('mail.message')
603         customer = False
604         if partner_id:
605             customer = partner.browse(cr, uid, partner_id, context=context)
606         for lead in self.browse(cr, uid, ids, context=context):
607             if lead.state in ('done', 'cancel'):
608                 continue
609             if user_ids or section_id:
610                 self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
611
612             vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
613             self.write(cr, uid, [lead.id], vals, context=context)
614
615             self.convert_opportunity_send_note(cr, uid, lead, context=context)
616             #TOCHECK: why need to change partner details in all messages of lead ?
617             if lead.partner_id:
618                 msg_ids = [ x.id for x in lead.message_ids]
619                 mail_message.write(cr, uid, msg_ids, {
620                         'partner_id': lead.partner_id.id
621                     }, context=context)
622         return True
623
624     def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
625         partner = self.pool.get('res.partner')
626         vals = { 'name': name,
627             'user_id': lead.user_id.id,
628             'comment': lead.description,
629             'section_id': lead.section_id.id or False,
630             'parent_id': parent_id,
631             'phone': lead.phone,
632             'mobile': lead.mobile,
633             'email': lead.email_from and to_email(lead.email_from)[0],
634             'fax': lead.fax,
635             'title': lead.title and lead.title.id or False,
636             'function': lead.function,
637             'street': lead.street,
638             'street2': lead.street2,
639             'zip': lead.zip,
640             'city': lead.city,
641             'country_id': lead.country_id and lead.country_id.id or False,
642             'state_id': lead.state_id and lead.state_id.id or False,
643             'is_company': is_company,
644             'type': 'contact'
645         }
646         partner = partner.create(cr, uid,vals, context)
647         return partner
648
649     def _create_lead_partner(self, cr, uid, lead, context=None):
650         partner_id =  False
651         if lead.partner_name and lead.contact_name:
652             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
653             self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
654         elif lead.partner_name and not lead.contact_name:
655             partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
656         elif not lead.partner_name and lead.contact_name:
657             partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
658         else:
659             partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
660         return partner_id
661
662     def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
663         res = False
664         res_partner = self.pool.get('res.partner')
665         if partner_id:
666             res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
667             contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
668             res = lead.write({'partner_id' : partner_id, }, context=context)
669             self._lead_set_partner_send_note(cr, uid, [lead.id], context)
670         return res
671
672     def convert_partner(self, cr, uid, ids, action='create', partner_id=False, context=None):
673         """
674         This function convert partner based on action.
675         if action is 'create', create new partner with contact and assign lead to new partner_id.
676         otherwise assign lead to specified partner_id
677         """
678         if context is None:
679             context = {}
680         partner_ids = {}
681         for lead in self.browse(cr, uid, ids, context=context):
682             if action == 'create':
683                 if not partner_id:
684                     partner_id = self._create_lead_partner(cr, uid, lead, context)
685             self._lead_set_partner(cr, uid, lead, partner_id, context=context)
686             partner_ids[lead.id] = partner_id
687         return partner_ids
688
689     def _send_mail_to_salesman(self, cr, uid, lead, context=None):
690         """
691         Send mail to salesman with updated Lead details.
692         @ lead: browse record of 'crm.lead' object.
693         """
694         #TOFIX: mail template should be used here instead of fix subject, body text.
695         message = self.pool.get('mail.message')
696         email_to = lead.user_id and lead.user_id.email
697         if not email_to:
698             return False
699
700         email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.email or email_to
701         partner = lead.partner_id and lead.partner_id.name or lead.partner_name
702         subject = "lead %s converted into opportunity" % lead.name
703         body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
704         return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
705
706
707     def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
708         index = 0
709         for lead_id in ids:
710             value = {}
711             if team_id:
712                 value['section_id'] = team_id
713             if index < len(user_ids):
714                 value['user_id'] = user_ids[index]
715                 index += 1
716             if value:
717                 self.write(cr, uid, [lead_id], value, context=context)
718         return True
719
720     def schedule_phonecall(self, cr, uid, ids, schedule_time, call_summary, desc, phone, contact_name, user_id=False, section_id=False, categ_id=False, action='schedule', context=None):
721         """
722         action :('schedule','Schedule a call'), ('log','Log a call')
723         """
724         phonecall = self.pool.get('crm.phonecall')
725         model_data = self.pool.get('ir.model.data')
726         phonecall_dict = {}
727         if not categ_id:
728             res_id = model_data._get_id(cr, uid, 'crm', 'categ_phone2')
729             if res_id:
730                 categ_id = model_data.browse(cr, uid, res_id, context=context).res_id
731         for lead in self.browse(cr, uid, ids, context=context):
732             if not section_id:
733                 section_id = lead.section_id and lead.section_id.id or False
734             if not user_id:
735                 user_id = lead.user_id and lead.user_id.id or False
736             vals = {
737                     'name' : call_summary,
738                     'opportunity_id' : lead.id,
739                     'user_id' : user_id or False,
740                     'categ_id' : categ_id or False,
741                     'description' : desc or '',
742                     'date' : schedule_time,
743                     'section_id' : section_id or False,
744                     'partner_id': lead.partner_id and lead.partner_id.id or False,
745                     'partner_phone' : phone or lead.phone or (lead.partner_id and lead.partner_id.phone or False),
746                     'partner_mobile' : lead.partner_id and lead.partner_id.mobile or False,
747                     'priority': lead.priority,
748             }
749             new_id = phonecall.create(cr, uid, vals, context=context)
750             phonecall.case_open(cr, uid, [new_id], context=context)
751             if action == 'log':
752                 phonecall.case_close(cr, uid, [new_id], context=context)
753             phonecall_dict[lead.id] = new_id
754             self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
755         return phonecall_dict
756
757
758     def redirect_opportunity_view(self, cr, uid, opportunity_id, context=None):
759         models_data = self.pool.get('ir.model.data')
760
761         # Get Opportunity views
762         form_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
763         tree_view = models_data.get_object_reference(cr, uid, 'crm', 'crm_case_tree_view_oppor')
764         return {
765                 'name': _('Opportunity'),
766                 'view_type': 'form',
767                 'view_mode': 'tree, form',
768                 'res_model': 'crm.lead',
769                 'domain': [('type', '=', 'opportunity')],
770                 'res_id': int(opportunity_id),
771                 'view_id': False,
772                 'views': [(form_view and form_view[1] or False, 'form'),
773                           (tree_view and tree_view[1] or False, 'tree'),
774                           (False, 'calendar'), (False, 'graph')],
775                 'type': 'ir.actions.act_window',
776         }
777
778     def action_makeMeeting(self, cr, uid, ids, context=None):
779         """ This opens Meeting's calendar view to schedule meeting on current Opportunity
780             @return : Dictionary value for created Meeting view
781         """
782         opportunity = self.browse(cr, uid, ids[0], context)
783         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
784         res['context'] = {
785             'default_opportunity_id': opportunity.id,
786             'default_partner_id': opportunity.partner_id and opportunity.partner_id.id or False,
787             'default_partner_ids' : opportunity.partner_id and [opportunity.partner_id.id] or False,
788             'default_user_id': uid,
789             'default_section_id': opportunity.section_id and opportunity.section_id.id or False,
790             'default_email_from': opportunity.email_from,
791             'default_state': 'open',
792             'default_name': opportunity.name,
793         }
794         return res
795
796     def unlink(self, cr, uid, ids, context=None):
797         for lead in self.browse(cr, uid, ids, context):
798             if (not lead.section_id.allow_unlink) and (lead.state != 'draft'):
799                 raise osv.except_osv(_('Error!'),
800                     _("You cannot delete lead '%s' because it is not in 'Draft' state. " \
801                       "You can still cancel it, instead of deleting it.") % lead.name)
802         return super(crm_lead, self).unlink(cr, uid, ids, context)
803
804     def write(self, cr, uid, ids, vals, context=None):
805         if vals.get('stage_id') and not vals.get('probability'):
806             # change probability of lead(s) if required by stage
807             stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
808             if stage.on_change:
809                 vals['probability'] = stage.probability
810         return super(crm_lead,self).write(cr, uid, ids, vals, context)
811
812     # ----------------------------------------
813     # Mail Gateway
814     # ----------------------------------------
815
816     def message_new(self, cr, uid, msg, custom_values=None, context=None):
817         """ Overrides mail_thread message_new that is called by the mailgateway
818             through message_process.
819             This override updates the document according to the email.
820         """
821         if custom_values is None: custom_values = {}
822         custom_values.update({
823             'name':  msg.get('subject') or _("No Subject"),
824             'description': msg.get('body_text'),
825             'email_from': msg.get('from'),
826             'email_cc': msg.get('cc'),
827             'user_id': False,
828         })
829         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
830             custom_values['priority'] = msg.get('priority')
831         custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context))
832         return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
833
834     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
835         """ Overrides mail_thread message_update that is called by the mailgateway
836             through message_process.
837             This method updates the document according to the email.
838         """
839         if isinstance(ids, (str, int, long)):
840             ids = [ids]
841         if update_vals is None: update_vals = {}
842
843         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
844             vals['priority'] = msg.get('priority')
845         maps = {
846             'cost':'planned_cost',
847             'revenue': 'planned_revenue',
848             'probability':'probability',
849         }
850         for line in msg.get('body_text', '').split('\n'):
851             line = line.strip()
852             res = tools.misc.command_re.match(line)
853             if res and maps.get(res.group(1).lower()):
854                 key = maps.get(res.group(1).lower())
855                 vals[key] = res.group(2).lower()
856
857         return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
858
859     # ----------------------------------------
860     # OpenChatter methods and notifications
861     # ----------------------------------------
862
863     def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
864         """ Add 'user_id' to the monitored fields """
865         res = super(crm_lead, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
866         return res + ['user_id']
867
868     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
869         """ Override of the (void) default notification method. """
870         stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
871         return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
872
873     def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
874         if isinstance(lead, (int, long)):
875             lead = self.browse(cr, uid, [lead], context=context)[0]
876         return ('Opportunity' if lead.type == 'opportunity' else 'Lead')
877
878     def create_send_note(self, cr, uid, ids, context=None):
879         for id in ids:
880             message = _("%s has been <b>created</b>.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
881             self.message_append_note(cr, uid, [id], body=message, context=context)
882         return True
883
884     def case_mark_lost_send_note(self, cr, uid, ids, context=None):
885         message = _("Opportunity has been <b>lost</b>.")
886         return self.message_append_note(cr, uid, ids, body=message, context=context)
887
888     def case_mark_won_send_note(self, cr, uid, ids, context=None):
889         message = _("Opportunity has been <b>won</b>.")
890         return self.message_append_note(cr, uid, ids, body=message, context=context)
891
892     def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
893         phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
894         if action == 'log': prefix = 'Logged'
895         else: prefix = 'Scheduled'
896         message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date)
897         return self.message_append_note(cr, uid, ids, body=message, context=context)
898
899     def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
900         for lead in self.browse(cr, uid, ids, context=context):
901             message = _("%s <b>partner</b> is now set to <em>%s</em>." % (self.case_get_note_msg_prefix(cr, uid, lead, context=context), lead.partner_id.name))
902             lead.message_append_note(body=message)
903         return True
904
905     def convert_opportunity_send_note(self, cr, uid, lead, context=None):
906         message = _("Lead has been <b>converted to an opportunity</b>.")
907         lead.message_append_note(body=message)
908         return True
909
910 crm_lead()
911
912 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: