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 for case in self.browse(cr, uid, ids, context):
126 st = not context.get('force_domain', False) and case.stage_id.id or False
128 data = {'stage_id': s[section][st]}
129 stage = s[section][st]
130 self.write(cr, uid, [case.id], data)
133 def get_stage_dict(self, cr, uid, ids, context=None):
134 """This function gives dictionary for stage according to stage levels
135 @param self: The object pointer
136 @param cr: the current row, from the database cursor,
137 @param uid: the current user’s ID for security checks,
138 @param ids: List of case IDs
139 @param context: A standard dictionary for contextual values"""
142 stage_obj = self.pool.get('crm.case.stage')
143 res = self.read(cr, uid, ids, ['section_id', 'stage_id'], context)[0]
144 section_id = res['section_id'] and res['section_id'][0] or False
145 stage_id = res['stage_id'] and res['stage_id'][0] or False
147 # We select either the stages in the same section as the current stage
148 # if it a stage that does not have a section, or the stages of the
149 # current section of the case
151 stage_record = stage_obj.browse(cr, uid, stage_id)
152 if not stage_record.section_id:
153 section_id = False # only select stages without section
155 domain = [('object_id.model', '=', self._name), ('section_id', '=', section_id)]
156 if 'force_domain' in context and context['force_domain']:
157 domain += context['force_domain']
158 sid = stage_obj.search(cr, uid, domain, context=context)
163 for stage in stage_obj.browse(cr, uid, sid, context=context):
164 s.setdefault(section, {})
165 s[section][previous.get(section, False)] = stage.id
166 previous[section] = stage.id
169 def stage_previous(self, cr, uid, ids, context=None):
170 """This function computes previous stage for case from its current stage
171 using available stage for that case type
172 @param self: The object pointer
173 @param cr: the current row, from the database cursor,
174 @param uid: the current user’s ID for security checks,
175 @param ids: List of case IDs
176 @param context: A standard dictionary for contextual values"""
180 s = self.get_stage_dict(cr, uid, ids, context=context)
182 stage_pool = self.pool.get('crm.case.stage')
183 for case in self.browse(cr, uid, ids, context):
185 st = not context.get('force_domain', False) and case.stage_id.id or False
186 s[section] = dict([(v, k) for (k, v) in s[section].iteritems()])
188 data = {'stage_id': s[section][st]}
190 stage = stage_pool.browse(cr, uid, s[section][st], context=context)
192 data.update({'probability': stage.probability})
193 self.write(cr, uid, [case.id], data)
196 def onchange_partner_id(self, cr, uid, ids, part, email=False):
197 """This function returns value of partner address based on partner
198 @param self: The object pointer
199 @param cr: the current row, from the database cursor,
200 @param uid: the current user’s ID for security checks,
201 @param ids: List of case IDs
202 @param part: Partner's id
203 @email: Partner's email ID
206 return {'value': {'partner_address_id': False,
210 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
211 data = {'partner_address_id': addr['contact']}
212 data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
213 return {'value': data}
215 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
216 """This function returns value of partner email based on Partner Address
217 @param self: The object pointer
218 @param cr: the current row, from the database cursor,
219 @param uid: the current user’s ID for security checks,
220 @param ids: List of case IDs
221 @param add: Id of Partner's address
222 @email: Partner's email ID
225 return {'value': {'email_from': False}}
226 address = self.pool.get('res.partner.address').browse(cr, uid, add)
227 return {'value': {'email_from': address.email, 'phone': address.phone}}
229 def _history(self, cr, uid, cases, keyword, history=False, subject=None, email=False, details=None, email_from=False, message_id=False, attach=[], context={}):
230 mailgate_pool = self.pool.get('mailgate.thread')
231 return mailgate_pool.history(cr, uid, cases, keyword, history=history,\
232 subject=subject, email=email, \
233 details=details, email_from=email_from,\
234 message_id=message_id, attach=attach, \
237 def case_open(self, cr, uid, ids, *args):
239 @param self: The object pointer
240 @param cr: the current row, from the database cursor,
241 @param uid: the current user’s ID for security checks,
242 @param ids: List of case Ids
243 @param *args: Tuple Value for additional Params
245 cases = self.browse(cr, uid, ids)
246 self._history(cr, uid, cases, _('Open'))
248 data = {'state': 'open', 'active': True}
250 data['user_id'] = uid
251 self.write(cr, uid, case.id, data)
252 self._action(cr, uid, cases, 'open')
255 def case_close(self, cr, uid, ids, *args):
257 @param self: The object pointer
258 @param cr: the current row, from the database cursor,
259 @param uid: the current user’s ID for security checks,
260 @param ids: List of case Ids
261 @param *args: Tuple Value for additional Params
263 cases = self.browse(cr, uid, ids)
264 cases[0].state # to fill the browse record cache
265 self._history(cr, uid, cases, _('Close'))
266 self.write(cr, uid, ids, {'state': 'done',
267 'date_closed': time.strftime('%Y-%m-%d %H:%M:%S'),
270 # We use the cache of cases to keep the old case state
272 self._action(cr, uid, cases, 'done')
275 def case_escalate(self, cr, uid, ids, *args):
276 """Escalates case to top level
277 @param self: The object pointer
278 @param cr: the current row, from the database cursor,
279 @param uid: the current user’s ID for security checks,
280 @param ids: List of case Ids
281 @param *args: Tuple Value for additional Params
283 cases = self.browse(cr, uid, ids)
285 data = {'active': True}
287 if case.section_id.parent_id:
288 data['section_id'] = case.section_id.parent_id.id
289 if case.section_id.parent_id.change_responsible:
290 if case.section_id.parent_id.user_id:
291 data['user_id'] = case.section_id.parent_id.user_id.id
293 raise osv.except_osv(_('Error !'), _('You can not escalate, You are already at the top level regarding your sales-team category.'))
294 self.write(cr, uid, [case.id], data)
295 cases = self.browse(cr, uid, ids)
296 self._history(cr, uid, cases, _('Escalate'))
297 self._action(cr, uid, cases, 'escalate')
300 def case_cancel(self, cr, uid, ids, *args):
302 @param self: The object pointer
303 @param cr: the current row, from the database cursor,
304 @param uid: the current user’s ID for security checks,
305 @param ids: List of case Ids
306 @param *args: Tuple Value for additional Params
308 cases = self.browse(cr, uid, ids)
309 cases[0].state # to fill the browse record cache
310 self._history(cr, uid, cases, _('Cancel'))
311 self.write(cr, uid, ids, {'state': 'cancel',
313 self._action(cr, uid, cases, 'cancel')
315 message = "The " + self._description + " '" + case.name + "' has been Cancelled."
316 #TODO: Need to differentiate lead and opportunity
317 # if hasattr(case, 'type'):
318 # #TO CHECK: hasattr gives warning for other crm objects that don't have field 'type'
319 # message = "The " + (case.type or 'Case').title() + " '" + case.name + "' has been Cancelled."
320 self.log(cr, uid, case.id, message)
323 def case_pending(self, cr, uid, ids, *args):
324 """Marks case as pending
325 @param self: The object pointer
326 @param cr: the current row, from the database cursor,
327 @param uid: the current user’s ID for security checks,
328 @param ids: List of case Ids
329 @param *args: Tuple Value for additional Params
331 cases = self.browse(cr, uid, ids)
332 cases[0].state # to fill the browse record cache
333 self._history(cr, uid, cases, _('Pending'))
334 self.write(cr, uid, ids, {'state': 'pending', 'active': True})
335 self._action(cr, uid, cases, 'pending')
338 def case_reset(self, cr, uid, ids, *args):
339 """Resets case as draft
340 @param self: The object pointer
341 @param cr: the current row, from the database cursor,
342 @param uid: the current user’s ID for security checks,
343 @param ids: List of case Ids
344 @param *args: Tuple Value for additional Params
346 cases = self.browse(cr, uid, ids)
347 cases[0].state # to fill the browse record cache
348 self._history(cr, uid, cases, _('Draft'))
349 self.write(cr, uid, ids, {'state': 'draft', 'active': True})
350 self._action(cr, uid, cases, 'draft')
353 def remind_partner(self, cr, uid, ids, context={}, attach=False):
356 @param self: The object pointer
357 @param cr: the current row, from the database cursor,
358 @param uid: the current user’s ID for security checks,
359 @param ids: List of Remind Partner's IDs
360 @param context: A standard dictionary for contextual values
363 return self.remind_user(cr, uid, ids, context, attach,
366 def remind_user(self, cr, uid, ids, context={}, attach=False,destination=True):
368 @param self: The object pointer
369 @param cr: the current row, from the database cursor,
370 @param uid: the current user’s ID for security checks,
371 @param ids: List of Remind user's IDs
372 @param context: A standard dictionary for contextual values
375 for case in self.browse(cr, uid, ids):
376 if not case.section_id.reply_to:
377 raise osv.except_osv(_('Error!'), ("Reply To is not specified in the sales team"))
378 if not case.email_from:
379 raise osv.except_osv(_('Error!'), ("Partner Email is not specified in Case"))
380 if case.section_id.reply_to and case.email_from:
381 src = case.email_from
382 dest = case.section_id.reply_to
383 body = case.description or ""
385 body = case.message_ids[0].description or ""
387 src, dest = dest, src
388 if body and case.user_id.signature:
390 body += '\n\n%s' % (case.user_id.signature)
392 body = '\n\n%s' % (case.user_id.signature)
394 body = self.format_body(body)
396 attach_to_send = None
399 attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
400 attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname','datas'])
401 attach_to_send = map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send)
404 subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
405 flag = tools.email_send(
410 reply_to=case.section_id.reply_to,
411 openobject_id=str(case.id),
412 attach=attach_to_send
414 self._history(cr, uid, [case], _('Send'), history=True, subject=subject, email=dest, details=body, email_from=src)
417 def _check(self, cr, uid, ids=False, context={}):
419 Function called by the scheduler to process cases for date actions
420 Only works on not done and cancelled cases
422 @param self: The object pointer
423 @param cr: the current row, from the database cursor,
424 @param uid: the current user’s ID for security checks,
425 @param context: A standard dictionary for contextual values
427 cr.execute('select * from crm_case \
428 where (date_action_last<%s or date_action_last is null) \
429 and (date_action_next<=%s or date_action_next is null) \
430 and state not in (\'cancel\',\'done\')',
431 (time.strftime("%Y-%m-%d %H:%M:%S"),
432 time.strftime('%Y-%m-%d %H:%M:%S')))
434 ids2 = map(lambda x: x[0], cr.fetchall() or [])
435 cases = self.browse(cr, uid, ids2, context)
436 return self._action(cr, uid, cases, False, context=context)
438 def _action(self, cr, uid, cases, state_to, scrit=None, context={}):
441 context['state_to'] = state_to
442 rule_obj = self.pool.get('base.action.rule')
443 model_obj = self.pool.get('ir.model')
444 model_ids = model_obj.search(cr, uid, [('model','=',self._name)])
445 rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])])
446 return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
448 def format_body(self, body):
449 return self.pool.get('base.action.rule').format_body(body)
451 def format_mail(self, obj, body):
452 return self.pool.get('base.action.rule').format_mail(obj, body)
454 class crm_case_section(osv.osv):
457 _name = "crm.case.section"
458 _description = "Sales Teams"
462 'name': fields.char('Sales Team', size=64, required=True, translate=True),
463 'code': fields.char('Code', size=8),
464 'active': fields.boolean('Active', help="If the active field is set to "\
465 "true, it will allow you to hide the sales team without removing it."),
466 'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
467 'change_responsible': fields.boolean('Change Responsible', help="Thick this box if you want that on escalation, the responsible of this sale team automatically becomes responsible of the lead/opportunity escaladed"),
468 'user_id': fields.many2one('res.users', 'Responsible User'),
469 'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'),
470 'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by Open ERP about cases in this sales team"),
471 'parent_id': fields.many2one('crm.case.section', 'Parent Team'),
472 'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'),
473 'resource_calendar_id': fields.many2one('resource.calendar', "Resource's Calendar"),
474 'note': fields.text('Description'),
475 'working_hours': fields.float('Working Hours', digits=(16,2 )),
479 'active': lambda *a: 1,
480 'allow_unlink': lambda *a: 1,
484 ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !')
487 def _check_recursion(self, cr, uid, ids):
490 Checks for recursion level for sales team
491 @param self: The object pointer
492 @param cr: the current row, from the database cursor,
493 @param uid: the current user’s ID for security checks,
494 @param ids: List of Sales team ids
499 cr.execute('select distinct parent_id from crm_case_section where id IN %s', (tuple(ids),))
500 ids = filter(None, map(lambda x: x[0], cr.fetchall()))
508 (_check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id'])
511 def name_get(self, cr, uid, ids, context=None):
512 """Overrides orm name_get method
513 @param self: The object pointer
514 @param cr: the current row, from the database cursor,
515 @param uid: the current user’s ID for security checks,
516 @param ids: List of sales team ids
524 reads = self.read(cr, uid, ids, ['name', 'parent_id'], context)
527 name = record['name']
528 if record['parent_id']:
529 name = record['parent_id'][1] + ' / ' + name
530 res.append((record['id'], name))
536 class crm_case_categ(osv.osv):
537 """ Category of Case """
539 _name = "crm.case.categ"
540 _description = "Category of case"
543 'name': fields.char('Case Category Name', size=64, required=True, translate=True),
544 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
545 'object_id': fields.many2one('ir.model', 'Object Name'),
548 def _find_object_id(self, cr, uid, context=None):
549 """Finds id for case object
550 @param self: The object pointer
551 @param cr: the current row, from the database cursor,
552 @param uid: the current user’s ID for security checks,
553 @param context: A standard dictionary for contextual values
556 object_id = context and context.get('object_id', False) or False
557 ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
558 return ids and ids[0]
561 'object_id' : _find_object_id
567 class crm_case_resource_type(osv.osv):
568 """ Resource Type of case """
570 _name = "crm.case.resource.type"
571 _description = "Resource Type of case"
575 'name': fields.char('Resource Type', size=64, required=True, translate=True),
576 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
577 'object_id': fields.many2one('ir.model', 'Object Name'),
579 def _find_object_id(self, cr, uid, context=None):
580 """Finds id for case object
581 @param self: The object pointer
582 @param cr: the current row, from the database cursor,
583 @param uid: the current user’s ID for security checks,
584 @param context: A standard dictionary for contextual values
586 object_id = context and context.get('object_id', False) or False
587 ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
588 return ids and ids[0]
591 'object_id' : _find_object_id
594 crm_case_resource_type()
597 class crm_case_stage(osv.osv):
598 """ Stage of case """
600 _name = "crm.case.stage"
601 _description = "Stage of case"
606 'name': fields.char('Stage Name', size=64, required=True, translate=True),
607 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
608 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of case stages."),
609 'object_id': fields.many2one('ir.model', 'Object Name'),
610 'probability': fields.float('Probability (%)', required=True),
611 'on_change': fields.boolean('Change Probability Automatically', \
612 help="Change Probability on next and previous stages."),
613 'requirements': fields.text('Requirements')
615 def _find_object_id(self, cr, uid, context=None):
616 """Finds id for case object
617 @param self: The object pointer
618 @param cr: the current row, from the database cursor,
619 @param uid: the current user’s ID for security checks,
620 @param context: A standard dictionary for contextual values
622 object_id = context and context.get('object_id', False) or False
623 ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
624 return ids and ids[0]
627 'sequence': lambda *args: 1,
628 'probability': lambda *args: 0.0,
629 'object_id' : _find_object_id
634 def _links_get(self, cr, uid, context=None):
635 """Gets links value for reference field
636 @param self: The object pointer
637 @param cr: the current row, from the database cursor,
638 @param uid: the current user’s ID for security checks,
639 @param context: A standard dictionary for contextual values
643 obj = self.pool.get('res.request.link')
644 ids = obj.search(cr, uid, [])
645 res = obj.read(cr, uid, ids, ['object', 'name'], context)
646 return [(r['object'], r['name']) for r in res]
648 class users(osv.osv):
649 _inherit = 'res.users'
650 _description = "Users"
652 'context_section_id': fields.many2one('crm.case.section', 'Sales Team'),
657 class res_partner(osv.osv):
658 _inherit = 'res.partner'
660 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
665 class crm_case_section_custom(osv.osv):
666 _name = "crm.case.section.custom"
667 _description = 'Custom CRM Case Section'
670 'name': fields.char('Case Section',size=64, required=True, translate=True),
671 'code': fields.char('Section Code',size=8),
672 'active': fields.boolean('Active'),
673 'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
674 'sequence': fields.integer('Sequence'),
675 'user_id': fields.many2one('res.users', 'Responsible User'),
676 'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by Open ERP about cases in this section"),
677 'parent_id': fields.many2one('crm.case.section.custom', 'Parent Section'),
678 'note': fields.text('Notes'),
687 ('code_uniq', 'unique (code)', 'The code of the section must be unique !')
690 def _check_recursion(self, cr, uid, ids):
693 cr.execute('SELECT DISTINCT parent_id FROM crm_case_section_custom '\
696 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
702 (_check_recursion, 'Error ! You cannot create recursive sections.', ['parent_id'])
705 crm_case_section_custom()
708 class crm_case_custom(osv.osv, crm_case):
709 _name = 'crm.case.custom'
710 _inherit = 'mailgate.thread'
711 _description = "Custom CRM Case"
714 'id': fields.integer('ID', readonly=True),
715 'name': fields.char('Name',size=64,required=True),
716 'priority': fields.selection(AVAILABLE_PRIORITIES, 'Priority'),
717 'active': fields.boolean('Active'),
718 'description': fields.text('Description'),
719 'section_id': fields.many2one('crm.case.section.custom', 'Section', required=True, select=True),
720 'probability': fields.float('Probability (%)'),
721 'email_from': fields.char('Partner Email', size=128),
722 'email_cc': fields.char('CC', size=252),
723 'partner_id': fields.many2one('res.partner', 'Partner'),
724 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
725 'date': fields.datetime('Date'),
726 'create_date': fields.datetime('Created' ,readonly=True),
727 'date_deadline': fields.datetime('Deadline'),
728 'date_closed': fields.datetime('Closed', readonly=True),
729 'user_id': fields.many2one('res.users', 'Responsible'),
730 'state': fields.selection(AVAILABLE_STATES, 'Status', size=16, readonly=True),
731 'ref' : fields.reference('Reference', selection=_links_get, size=128),
732 'date_action_last': fields.datetime('Last Action', readonly=1),
733 'date_action_next': fields.datetime('Next Action', readonly=1),
739 'priority': AVAILABLE_PRIORITIES[2][0],
740 'date': time.strftime('%Y-%m-%d %H:%M:%S'),
746 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: