[IMP] sale,purchase, stock,crm : merge sales orders, merge purchase orders, merge...
[odoo/odoo.git] / addons / crm / crm_lead.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import fields, osv
23 from datetime import datetime
24 import crm
25 import time
26 from tools.translate import _
27 from crm import crm_case
28 import binascii
29 import tools
30
31
32 CRM_LEAD_PENDING_STATES = (
33     crm.AVAILABLE_STATES[2][0], # Cancelled
34     crm.AVAILABLE_STATES[3][0], # Done
35     crm.AVAILABLE_STATES[4][0], # Pending
36 )
37
38 class crm_lead(crm_case, osv.osv):
39     """ CRM Lead Case """
40     _name = "crm.lead"
41     _description = "Lead"
42     _order = "date_action, priority, id desc"
43     _inherit = ['mailgate.thread','res.partner.address']
44     
45     def fields_view_get(self, cr, uid, view_id=None, view_type='form',
46                             context=None, toolbar=False, submenu=False):
47             """
48              Changes the view dynamically
49              @param self: The object pointer.
50              @param cr: A database cursor
51              @param uid: ID of the user currently logged in
52              @param context: A standard dictionary
53              @return: New arch of view.
54             """
55             if not context:
56                 context={}
57             res = super(crm_lead, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar,submenu=False)
58             if view_type == 'form':
59                if res['toolbar']['action'] and res['toolbar']['action'][0]['res_model']=='crm.merge.opportunity':
60                    res['toolbar']['action']=[]
61             return res
62
63     def _compute_day(self, cr, uid, ids, fields, args, context={}):
64         """
65         @param cr: the current row, from the database cursor,
66         @param uid: the current user’s ID for security checks,
67         @param ids: List of Openday’s IDs
68         @return: difference between current date and log date
69         @param context: A standard dictionary for contextual values
70         """
71         cal_obj = self.pool.get('resource.calendar')
72         res_obj = self.pool.get('resource.resource')
73
74         res = {}
75         for lead in self.browse(cr, uid, ids , context):
76             for field in fields:
77                 res[lead.id] = {}
78                 duration = 0
79                 ans = False
80                 if field == 'day_open':
81                     if lead.date_open:
82                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
83                         date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S")
84                         ans = date_open - date_create
85                         date_until = lead.date_open
86                 elif field == 'day_close':
87                     if lead.date_closed:
88                         date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S")
89                         date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S")
90                         date_until = lead.date_closed
91                         ans = date_close - date_create
92                 if ans:
93                     resource_id = False
94                     if lead.user_id:
95                         resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)])
96                         if len(resource_ids):
97                             resource_id = resource_ids[0]
98
99                     duration = float(ans.days)
100                     if lead.section_id and lead.section_id.resource_calendar_id:
101                         duration =  float(ans.days) * 24
102                         new_dates = cal_obj.interval_get(cr,
103                             uid,
104                             lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False,
105                             datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'),
106                             duration,
107                             resource=resource_id
108                         )
109                         no_days = []
110                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
111                         for in_time, out_time in new_dates:
112                             if in_time.date not in no_days:
113                                 no_days.append(in_time.date)
114                             if out_time > date_until:
115                                 break
116                         duration =  len(no_days)
117                 res[lead.id][field] = abs(int(duration))
118         return res
119
120     _columns = {
121         # Overridden from res.partner.address:
122         'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', 
123             select=True, help="Optional linked partner, usually after conversion of the lead"),
124
125         # From crm.case
126         'id': fields.integer('ID'),
127         'name': fields.char('Name', size=64),
128         'active': fields.boolean('Active', required=False),
129         'date_action_last': fields.datetime('Last Action', readonly=1),
130         'date_action_next': fields.datetime('Next Action', readonly=1),
131         'email_from': fields.char('Email', size=128, help="E-mail address of the contact"),
132         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
133                         select=True, help='Sales team to which this case belongs to. Defines responsible user and e-mail address for the mail gateway.'),
134         'create_date': fields.datetime('Creation Date' , readonly=True),
135         '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"),
136         'description': fields.text('Notes'),
137         'write_date': fields.datetime('Update Date' , readonly=True),
138
139         # Lead fields
140         'categ_id': fields.many2one('crm.case.categ', 'Category', \
141             domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.project.bug')]"),
142         'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
143             domain="['|',('section_id','=',section_id),('section_id','=',False)]"),
144         'channel_id': fields.many2one('res.partner.canal', 'Channel'),
145
146         'contact_name': fields.char('Contact Name', size=64), 
147         'partner_name': fields.char("Customer Name", size=64),
148         'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."),
149         'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."),
150         'type':fields.selection([
151             ('lead','Lead'),
152             ('opportunity','Opportunity'),
153
154         ],'Type', help="Type is used to separate Leads and Opportunities"),
155         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
156         'date_closed': fields.datetime('Closed', readonly=True),
157         'stage_id': fields.many2one('crm.case.stage', 'Stage'),
158         'user_id': fields.many2one('res.users', 'Salesman',help='By Default Salesman is Administrator when create New User'),
159         'referred': fields.char('Referred By', size=64),
160         'date_open': fields.datetime('Opened', readonly=True),
161         'day_open': fields.function(_compute_day, string='Days to Open', \
162                                 method=True, multi='day_open', type="float", store=True),
163         'day_close': fields.function(_compute_day, string='Days to Close', \
164                                 method=True, multi='day_close', type="float", store=True),
165         'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
166                                   help='The state is set to \'Draft\', when a case is created.\
167                                   \nIf the case is in progress the state is set to \'Open\'.\
168                                   \nWhen the case is over, the state is set to \'Done\'.\
169                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'), 
170         'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
171     }
172
173     _defaults = {
174         'active': lambda *a: 1,
175         'user_id': crm_case._get_default_user,
176         'email_from': crm_case._get_default_email,
177         'state': lambda *a: 'draft',
178         'type': lambda *a: 'lead',
179         'section_id': crm_case._get_section,
180         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
181         'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
182     }
183
184     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
185         """This function returns value of partner email based on Partner Address
186         @param self: The object pointer
187         @param cr: the current row, from the database cursor,
188         @param uid: the current user’s ID for security checks,
189         @param ids: List of case IDs
190         @param add: Id of Partner's address
191         @email: Partner's email ID
192         """
193         if not add:
194             return {'value': {'email_from': False, 'country_id': False}}
195         address = self.pool.get('res.partner.address').browse(cr, uid, add)
196         return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}}
197
198     def case_open(self, cr, uid, ids, *args):
199         """Overrides cancel for crm_case for setting Open Date
200         @param self: The object pointer
201         @param cr: the current row, from the database cursor,
202         @param uid: the current user’s ID for security checks,
203         @param ids: List of case's Ids
204         @param *args: Give Tuple Value
205         """
206         old_state = self.read(cr, uid, ids, ['state'])[0]['state']
207         old_stage_id = self.read(cr, uid, ids, ['stage_id'])[0]['stage_id']
208         res = super(crm_lead, self).case_open(cr, uid, ids, *args)
209         if old_state == 'draft':
210             value = {}
211             if not old_stage_id:
212                 stage_id = super(crm_lead, self).stage_next(cr, uid, ids, *args)
213                 if stage_id:
214                     value.update({'stage_id': stage_id})
215                     value.update(self.onchange_stage_id(cr, uid, ids, stage_id, context={})['value'])
216             value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S')})
217             self.write(cr, uid, ids, value)
218
219         for (id, name) in self.name_get(cr, uid, ids):
220             type = self.browse(cr, uid, id).type or 'Lead'
221             message = (_('The ') + type.title()) + " '" + name + "' "+ _("has been Opened.")
222             self.log(cr, uid, id, message)
223         return res
224
225     def case_close(self, cr, uid, ids, *args):
226         """Overrides close for crm_case for setting close date
227         @param self: The object pointer
228         @param cr: the current row, from the database cursor,
229         @param uid: the current user’s ID for security checks,
230         @param ids: List of case Ids
231         @param *args: Tuple Value for additional Params
232         """
233         res = super(crm_lead, self).case_close(cr, uid, ids, args)
234         self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
235         for (id, name) in self.name_get(cr, uid, ids):
236             lead = self.browse(cr, uid, id)
237             if lead.type == 'lead':
238                 message = _('The Lead') + " '" + name + "' "+ _("has been Closed.")
239                 self.log(cr, uid, id, message)
240         return res
241
242     def convert_opportunity(self, cr, uid, ids, context=None):
243         """ Precomputation for converting lead to opportunity
244         @param cr: the current row, from the database cursor,
245         @param uid: the current user’s ID for security checks,
246         @param ids: List of closeday’s IDs
247         @param context: A standard dictionary for contextual values
248         @return: Value of action in dict
249         """
250         if not context:
251             context = {}
252         context.update({'active_ids': ids})
253
254         data_obj = self.pool.get('ir.model.data')
255         data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_action')
256         value = {}
257
258         view_id = False
259         if data_id:
260             view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
261
262         for case in self.browse(cr, uid, ids):
263             context.update({'active_id': case.id})
264             if not case.partner_id:
265                 data_id = data_obj._get_id(cr, uid, 'crm', 'view_crm_lead2opportunity_partner')
266                 view_id1 = False
267                 if data_id:
268                     view_id1 = data_obj.browse(cr, uid, data_id, context=context).res_id
269                 value = {
270                         'name': _('Create Partner'),
271                         'view_type': 'form',
272                         'view_mode': 'form,tree',
273                         'res_model': 'crm.lead2opportunity.partner',
274                         'view_id': False,
275                         'context': context,
276                         'views': [(view_id1, 'form')],
277                         'type': 'ir.actions.act_window',
278                         'target': 'new',
279                         'nodestroy': True
280                         }
281                 break
282             else:
283                 value = {
284                         'name': _('Create Opportunity'),
285                         'view_type': 'form',
286                         'view_mode': 'form,tree',
287                         'res_model': 'crm.lead2opportunity.action',
288                         'view_id': False,
289                         'context': context,
290                         'views': [(view_id, 'form')],
291                         'type': 'ir.actions.act_window',
292                         'target': 'new',
293                         'nodestroy': True
294                         }
295         return value
296
297     def stage_next(self, cr, uid, ids, context=None):
298         stage = super(crm_lead, self).stage_next(cr, uid, ids, context)
299         if stage:
300             stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context)
301             if stage_obj.on_change:
302                 data = {'probability': stage_obj.probability}
303                 self.write(cr, uid, ids, data)
304         return stage
305
306     def message_new(self, cr, uid, msg, context):
307         """
308         Automatically calls when new email message arrives
309
310         @param self: The object pointer
311         @param cr: the current row, from the database cursor,
312         @param uid: the current user’s ID for security checks
313         """
314
315         mailgate_pool = self.pool.get('email.server.tools')
316
317         subject = msg.get('subject')
318         body = msg.get('body')
319         msg_from = msg.get('from')
320         priority = msg.get('priority')
321
322         vals = {
323             'name': subject,
324             'email_from': msg_from,
325             'email_cc': msg.get('cc'),
326             'description': body,
327             'user_id': False,
328         }
329         if msg.get('priority', False):
330             vals['priority'] = priority
331
332         res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
333         if res:
334             vals.update(res)
335
336         res = self.create(cr, uid, vals, context)
337         message = _('A Lead created') + " '" + subject + "' " + _("from Mailgate.")
338         self.log(cr, uid, res, message)
339         attachents = msg.get('attachments', [])
340         for attactment in attachents or []:
341             data_attach = {
342                 'name': attactment,
343                 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
344                 'datas_fname': attactment,
345                 'description': 'Mail attachment',
346                 'res_model': self._name,
347                 'res_id': res,
348             }
349             self.pool.get('ir.attachment').create(cr, uid, data_attach)
350
351         return res
352
353     def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context={}):
354         """
355         @param self: The object pointer
356         @param cr: the current row, from the database cursor,
357         @param uid: the current user’s ID for security checks,
358         @param ids: List of update mail’s IDs 
359         """
360
361         if isinstance(ids, (str, int, long)):
362             ids = [ids]
363
364         if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
365             vals['priority'] = msg.get('priority')
366
367         maps = {
368             'cost':'planned_cost',
369             'revenue': 'planned_revenue',
370             'probability':'probability'
371         }
372         vls = {}
373         for line in msg['body'].split('\n'):
374             line = line.strip()
375             res = tools.misc.command_re.match(line)
376             if res and maps.get(res.group(1).lower()):
377                 key = maps.get(res.group(1).lower())
378                 vls[key] = res.group(2).lower()
379         vals.update(vls)
380
381         # Unfortunately the API is based on lists
382         # but we want to update the state based on the
383         # previous state, so we have to loop:
384         for case in self.browse(cr, uid, ids, context=context):
385             values = dict(vals)
386             if case.state in CRM_LEAD_PENDING_STATES:
387                 values.update(state=crm.AVAILABLE_STATES[1][0]) #re-open
388             res = self.write(cr, uid, [case.id], values, context=context)
389
390         return res
391
392     def msg_send(self, cr, uid, id, *args, **argv):
393
394         """ Send The Message
395             @param self: The object pointer
396             @param cr: the current row, from the database cursor,
397             @param uid: the current user’s ID for security checks,
398             @param ids: List of email’s IDs
399             @param *args: Return Tuple Value
400             @param **args: Return Dictionary of Keyword Value
401         """
402         return True
403 crm_lead()
404
405 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: