1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
23 from datetime import datetime
24 from datetime import timedelta
27 from osv import fields
29 from tools.translate import _
35 ('cancel', 'Cancelled'),
37 ('pending', 'Pending'),
40 AVAILABLE_PRIORITIES = [
48 class crm_case(object):
49 """A simple python class to be used for common functions """
51 def _get_default_partner_address(self, cr, uid, context):
52 """Gives id of default address for current user
53 @param self: The object pointer
54 @param cr: the current row, from the database cursor,
55 @param uid: the current user’s ID for security checks,
56 @param context: A standard dictionary for contextual values
58 if not context.get('portal', False):
60 return self.pool.get('res.users').browse(cr, uid, uid, context).address_id.id
62 def _get_default_partner(self, cr, uid, context):
63 """Gives id of partner for current user
64 @param self: The object pointer
65 @param cr: the current row, from the database cursor,
66 @param uid: the current user’s ID for security checks,
67 @param context: A standard dictionary for contextual values
69 if not context.get('portal', False):
71 user = self.pool.get('res.users').browse(cr, uid, uid, context)
72 if not user.address_id:
74 return user.address_id.partner_id.id
76 def _get_default_email(self, cr, uid, context):
77 """Gives default email address for current user
78 @param self: The object pointer
79 @param cr: the current row, from the database cursor,
80 @param uid: the current user’s ID for security checks,
81 @param context: A standard dictionary for contextual values
83 if not context.get('portal', False):
85 user = self.pool.get('res.users').browse(cr, uid, uid, context)
86 if not user.address_id:
88 return user.address_id.email
90 def _get_default_user(self, cr, uid, context):
91 """Gives current user id
92 @param self: The object pointer
93 @param cr: the current row, from the database cursor,
94 @param uid: the current user’s ID for security checks,
95 @param context: A standard dictionary for contextual values
97 if context.get('portal', False):
101 def _get_section(self, cr, uid, context):
102 """Gives section id for current User
103 @param self: The object pointer
104 @param cr: the current row, from the database cursor,
105 @param uid: the current user’s ID for security checks,
106 @param context: A standard dictionary for contextual values
108 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
109 return user.context_section_id.id or False
111 def stage_next(self, cr, uid, ids, context=None):
112 """This function computes next stage for case from its current stage
113 using available stage for that case type
114 @param self: The object pointer
115 @param cr: the current row, from the database cursor,
116 @param uid: the current user’s ID for security checks,
117 @param ids: List of case IDs
118 @param context: A standard dictionary for contextual values"""
121 s = self.get_stage_dict(cr, uid, ids, context=context)
124 stage_pool = self.pool.get('crm.case.stage')
125 for case in self.browse(cr, uid, ids, context):
127 st = case.stage_id.id or False
129 data = {'stage_id': s[section][st]}
130 stage = s[section][st]
131 self.write(cr, uid, [case.id], data)
134 def get_stage_dict(self, cr, uid, ids, context=None):
135 """This function gives dictionary for stage according to stage levels
136 @param self: The object pointer
137 @param cr: the current row, from the database cursor,
138 @param uid: the current user’s ID for security checks,
139 @param ids: List of case IDs
140 @param context: A standard dictionary for contextual values"""
143 stage_obj = self.pool.get('crm.case.stage')
144 sid = stage_obj.search(cr, uid, \
145 [('object_id.model', '=', self._name)], context=context)
149 for stage in stage_obj.browse(cr, uid, sid, context=context):
150 s.setdefault(section, {})
151 s[section][previous.get(section, False)] = stage.id
152 previous[section] = stage.id
155 def stage_previous(self, cr, uid, ids, context=None):
156 """This function computes previous stage for case from its current stage
157 using available stage for that case type
158 @param self: The object pointer
159 @param cr: the current row, from the database cursor,
160 @param uid: the current user’s ID for security checks,
161 @param ids: List of case IDs
162 @param context: A standard dictionary for contextual values"""
166 s = self.get_stage_dict(cr, uid, ids, context=context)
168 stage_pool = self.pool.get('crm.case.stage')
169 for case in self.browse(cr, uid, ids, context):
171 st = case.stage_id.id or False
172 s[section] = dict([(v, k) for (k, v) in s[section].iteritems()])
174 data = {'stage_id': s[section][st]}
176 stage = stage_pool.browse(cr, uid, s[section][st], context=context)
178 data.update({'probability': stage.probability})
179 self.write(cr, uid, [case.id], data)
182 def onchange_partner_id(self, cr, uid, ids, part, email=False):
183 """This function returns value of partner address based on partner
184 @param self: The object pointer
185 @param cr: the current row, from the database cursor,
186 @param uid: the current user’s ID for security checks,
187 @param ids: List of case IDs
188 @param part: Partner's id
189 @email: Partner's email ID
192 return {'value': {'partner_address_id': False,
196 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
197 data = {'partner_address_id': addr['contact']}
198 data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
199 return {'value': data}
201 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
202 """This function returns value of partner email based on Partner Address
203 @param self: The object pointer
204 @param cr: the current row, from the database cursor,
205 @param uid: the current user’s ID for security checks,
206 @param ids: List of case IDs
207 @param add: Id of Partner's address
208 @email: Partner's email ID
211 return {'value': {'email_from': False}}
212 address = self.pool.get('res.partner.address').browse(cr, uid, add)
213 return {'value': {'email_from': address.email, 'phone': address.phone}}
215 def _history(self, cr, uid, cases, keyword, history=False, subject=None, email=False, details=None, email_from=False, message_id=False, attach=[], context={}):
216 mailgate_pool = self.pool.get('mailgate.thread')
217 return mailgate_pool.history(cr, uid, cases, keyword, history=history,\
218 subject=subject, email=email, \
219 details=details, email_from=email_from,\
220 message_id=message_id, attach=attach, \
223 def case_open(self, cr, uid, ids, *args):
225 @param self: The object pointer
226 @param cr: the current row, from the database cursor,
227 @param uid: the current user’s ID for security checks,
228 @param ids: List of case Ids
229 @param *args: Tuple Value for additional Params
231 cases = self.browse(cr, uid, ids)
232 self._history(cr, uid, cases, _('Open'))
234 data = {'state': 'open', 'active': True}
236 data['user_id'] = uid
237 self.write(cr, uid, case.id, data)
238 self._action(cr, uid, cases, 'open')
241 def case_close(self, cr, uid, ids, *args):
243 @param self: The object pointer
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 case Ids
247 @param *args: Tuple Value for additional Params
249 cases = self.browse(cr, uid, ids)
250 cases[0].state # to fill the browse record cache
251 self._history(cr, uid, cases, _('Close'))
252 self.write(cr, uid, ids, {'state': 'done',
253 'date_closed': time.strftime('%Y-%m-%d %H:%M:%S'),
256 # We use the cache of cases to keep the old case state
258 self._action(cr, uid, cases, 'done')
261 def case_escalate(self, cr, uid, ids, *args):
262 """Escalates case to top level
263 @param self: The object pointer
264 @param cr: the current row, from the database cursor,
265 @param uid: the current user’s ID for security checks,
266 @param ids: List of case Ids
267 @param *args: Tuple Value for additional Params
269 cases = self.browse(cr, uid, ids)
271 data = {'active': True, 'user_id': False}
273 if case.section_id.parent_id:
274 data['section_id'] = case.section_id.parent_id.id
275 if case.section_id.parent_id.user_id:
276 data['user_id'] = case.section_id.parent_id.user_id.id
278 raise osv.except_osv(_('Error !'), _('You can not escalate, You are already at the top level regarding your sales-team category.'))
279 self.write(cr, uid, [case.id], data)
280 cases = self.browse(cr, uid, ids)
281 self._history(cr, uid, cases, _('Escalate'))
282 self._action(cr, uid, cases, 'escalate')
285 def case_cancel(self, cr, uid, ids, *args):
287 @param self: The object pointer
288 @param cr: the current row, from the database cursor,
289 @param uid: the current user’s ID for security checks,
290 @param ids: List of case Ids
291 @param *args: Tuple Value for additional Params
293 cases = self.browse(cr, uid, ids)
294 cases[0].state # to fill the browse record cache
295 self._history(cr, uid, cases, _('Cancel'))
296 self.write(cr, uid, ids, {'state': 'cancel',
298 self._action(cr, uid, cases, 'cancel')
301 def case_pending(self, cr, uid, ids, *args):
302 """Marks case as pending
303 @param self: The object pointer
304 @param cr: the current row, from the database cursor,
305 @param uid: the current user’s ID for security checks,
306 @param ids: List of case Ids
307 @param *args: Tuple Value for additional Params
309 cases = self.browse(cr, uid, ids)
310 cases[0].state # to fill the browse record cache
311 self._history(cr, uid, cases, _('Pending'))
312 self.write(cr, uid, ids, {'state': 'pending', 'active': True})
313 self._action(cr, uid, cases, 'pending')
316 def case_reset(self, cr, uid, ids, *args):
317 """Resets case as draft
318 @param self: The object pointer
319 @param cr: the current row, from the database cursor,
320 @param uid: the current user’s ID for security checks,
321 @param ids: List of case Ids
322 @param *args: Tuple Value for additional Params
324 cases = self.browse(cr, uid, ids)
325 cases[0].state # to fill the browse record cache
326 self._history(cr, uid, cases, _('Draft'))
327 self.write(cr, uid, ids, {'state': 'draft', 'active': True})
328 self._action(cr, uid, cases, 'draft')
331 def remind_partner(self, cr, uid, ids, context={}, attach=False):
334 @param self: The object pointer
335 @param cr: the current row, from the database cursor,
336 @param uid: the current user’s ID for security checks,
337 @param ids: List of Remind Partner's IDs
338 @param context: A standard dictionary for contextual values
341 return self.remind_user(cr, uid, ids, context, attach,
344 def remind_user(self, cr, uid, ids, context={}, attach=False,destination=True):
346 @param self: The object pointer
347 @param cr: the current row, from the database cursor,
348 @param uid: the current user’s ID for security checks,
349 @param ids: List of Remind user's IDs
350 @param context: A standard dictionary for contextual values
353 for case in self.browse(cr, uid, ids):
354 if not case.section_id.reply_to:
355 raise osv.except_osv(_('Error!'), ("Reply To is not specified in the sales team"))
356 if not case.email_from:
357 raise osv.except_osv(_('Error!'), ("Partner Email is not specified in Case"))
358 if case.section_id.reply_to and case.email_from:
359 src = case.email_from
360 dest = case.section_id.reply_to
361 body = case.description or ""
363 body = case.message_ids[0].description or ""
365 src, dest = dest, src
366 if body and case.user_id.signature:
367 body += '\n\n%s' % (case.user_id.signature)
369 body = self.format_body(body)
371 attach_to_send = None
374 attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
375 attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname','datas'])
376 attach_to_send = map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send)
379 subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
380 flag = tools.email_send(
385 reply_to=case.section_id.reply_to,
386 openobject_id=str(case.id),
387 attach=attach_to_send
389 self._history(cr, uid, [case], _('Send'), history=True, subject=subject, email=dest, details=body, email_from=src)
392 def _check(self, cr, uid, ids=False, context={}):
394 Function called by the scheduler to process cases for date actions
395 Only works on not done and cancelled cases
397 @param self: The object pointer
398 @param cr: the current row, from the database cursor,
399 @param uid: the current user’s ID for security checks,
400 @param context: A standard dictionary for contextual values
402 cr.execute('select * from crm_case \
403 where (date_action_last<%s or date_action_last is null) \
404 and (date_action_next<=%s or date_action_next is null) \
405 and state not in (\'cancel\',\'done\')',
406 (time.strftime("%Y-%m-%d %H:%M:%S"),
407 time.strftime('%Y-%m-%d %H:%M:%S')))
409 ids2 = map(lambda x: x[0], cr.fetchall() or [])
410 cases = self.browse(cr, uid, ids2, context)
411 return self._action(cr, uid, cases, False, context=context)
413 def _action(self, cr, uid, cases, state_to, scrit=None, context={}):
416 context['state_to'] = state_to
417 rule_obj = self.pool.get('base.action.rule')
418 model_obj = self.pool.get('ir.model')
419 model_ids = model_obj.search(cr, uid, [('model','=',self._name)])
420 rule_ids = rule_obj.search(cr, uid, [('name','=',model_ids[0])])
421 return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
423 def format_body(self, body):
424 return self.pool.get('base.action.rule').format_body(body)
426 def format_mail(self, obj, body):
427 return self.pool.get('base.action.rule').format_mail(obj, body)
429 class crm_case_section(osv.osv):
432 _name = "crm.case.section"
433 _description = "Sales Teams"
437 'name': fields.char('Sales Team', size=64, required=True, translate=True),
438 'code': fields.char('Code', size=8),
439 'active': fields.boolean('Active', help="If the active field is set to \
440 true, it will allow you to hide the sales team without removing it."),
441 'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
442 'user_id': fields.many2one('res.users', 'Responsible User'),
443 'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'),
444 'reply_to': fields.char('Reply-To', size=64, help="The email address put \
445 in the 'Reply-To' of all emails sent by Open ERP about cases in this sales team"),
446 'parent_id': fields.many2one('crm.case.section', 'Parent Team'),
447 'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'),
448 'resource_calendar_id': fields.many2one('resource.calendar', "Resource's Calendar"),
449 'note': fields.text('Description'),
450 'working_hours': fields.float('Working Hours', digits=(16,2 )),
454 'active': lambda *a: 1,
455 'allow_unlink': lambda *a: 1,
459 ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !')
462 def _check_recursion(self, cr, uid, ids):
465 Checks for recursion level for sales team
466 @param self: The object pointer
467 @param cr: the current row, from the database cursor,
468 @param uid: the current user’s ID for security checks,
469 @param ids: List of Sales team ids
474 cr.execute('select distinct parent_id from crm_case_section where id IN %s', (tuple(ids),))
475 ids = filter(None, map(lambda x: x[0], cr.fetchall()))
483 (_check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id'])
486 def name_get(self, cr, uid, ids, context=None):
487 """Overrides orm name_get method
488 @param self: The object pointer
489 @param cr: the current row, from the database cursor,
490 @param uid: the current user’s ID for security checks,
491 @param ids: List of sales team ids
499 reads = self.read(cr, uid, ids, ['name', 'parent_id'], context)
502 name = record['name']
503 if record['parent_id']:
504 name = record['parent_id'][1] + ' / ' + name
505 res.append((record['id'], name))
511 class crm_case_categ(osv.osv):
512 """ Category of Case """
514 _name = "crm.case.categ"
515 _description = "Category of case"
518 'name': fields.char('Case Category Name', size=64, required=True, translate=True),
519 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
520 'object_id': fields.many2one('ir.model', 'Object Name'),
523 def _find_object_id(self, cr, uid, context=None):
524 """Finds id for case object
525 @param self: The object pointer
526 @param cr: the current row, from the database cursor,
527 @param uid: the current user’s ID for security checks,
528 @param context: A standard dictionary for contextual values
531 object_id = context and context.get('object_id', False) or False
532 ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
533 return ids and ids[0]
536 'object_id' : _find_object_id
542 class crm_case_resource_type(osv.osv):
543 """ Resource Type of case """
545 _name = "crm.case.resource.type"
546 _description = "Resource Type of case"
550 'name': fields.char('Resource Type', size=64, required=True, translate=True),
551 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
552 'object_id': fields.many2one('ir.model', 'Object Name'),
554 def _find_object_id(self, cr, uid, context=None):
555 """Finds id for case object
556 @param self: The object pointer
557 @param cr: the current row, from the database cursor,
558 @param uid: the current user’s ID for security checks,
559 @param context: A standard dictionary for contextual values
561 object_id = context and context.get('object_id', False) or False
562 ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
563 return ids and ids[0]
566 'object_id' : _find_object_id
569 crm_case_resource_type()
572 class crm_case_stage(osv.osv):
573 """ Stage of case """
575 _name = "crm.case.stage"
576 _description = "Stage of case"
581 'name': fields.char('Stage Name', size=64, required=True, translate=True),
582 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
583 'sequence': fields.integer('Sequence', help="Gives the sequence order \
584 when displaying a list of case stages."),
585 'object_id': fields.many2one('ir.model', 'Object Name'),
586 'probability': fields.float('Probability (%)', required=True),
587 'on_change': fields.boolean('Change Probability Automatically', \
588 help="Change Probability on next and previous stages."),
589 'requirements': fields.text('Requirements')
591 def _find_object_id(self, cr, uid, context=None):
592 """Finds id for case object
593 @param self: The object pointer
594 @param cr: the current row, from the database cursor,
595 @param uid: the current user’s ID for security checks,
596 @param context: A standard dictionary for contextual values
598 object_id = context and context.get('object_id', False) or False
599 ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
600 return ids and ids[0]
603 'sequence': lambda *args: 1,
604 'probability': lambda *args: 0.0,
605 'object_id' : _find_object_id
610 def _links_get(self, cr, uid, context=None):
611 """Gets links value for reference field
612 @param self: The object pointer
613 @param cr: the current row, from the database cursor,
614 @param uid: the current user’s ID for security checks,
615 @param context: A standard dictionary for contextual values
619 obj = self.pool.get('res.request.link')
620 ids = obj.search(cr, uid, [])
621 res = obj.read(cr, uid, ids, ['object', 'name'], context)
622 return [(r['object'], r['name']) for r in res]
624 class users(osv.osv):
625 _inherit = 'res.users'
626 _description = "Users"
628 'context_section_id': fields.many2one('crm.case.section', 'Sales Team'),
633 class res_partner(osv.osv):
634 _inherit = 'res.partner'
636 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
640 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: