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