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 ##############################################################################
25 from osv import fields
27 from tools.translate import _
33 ('cancel', 'Cancelled'),
35 ('pending', 'Pending'),
38 AVAILABLE_PRIORITIES = [
46 class crm_case(object):
47 """A simple python class to be used for common functions """
49 def _get_default_partner_address(self, cr, uid, context):
50 """Gives id of default address for current user
51 @param self: The object pointer
52 @param cr: the current row, from the database cursor,
53 @param uid: the current user’s ID for security checks,
54 @param context: A standard dictionary for contextual values
56 if not context.get('portal', False):
58 return self.pool.get('res.users').browse(cr, uid, uid, context).address_id.id
60 def _get_default_partner(self, cr, uid, context):
61 """Gives id of partner for current user
62 @param self: The object pointer
63 @param cr: the current row, from the database cursor,
64 @param uid: the current user’s ID for security checks,
65 @param context: A standard dictionary for contextual values
67 if not context.get('portal', False):
69 user = self.pool.get('res.users').browse(cr, uid, uid, context)
70 if not user.address_id:
72 return user.address_id.partner_id.id
74 def copy(self, cr, uid, id, default=None, context=None):
76 Overrides orm copy method.
77 @param self: the object pointer
78 @param cr: the current row, from the database cursor,
79 @param uid: the current user’s ID for security checks,
80 @param id: Id of mailgate thread
81 @param default: Dictionary of default values for copy.
82 @param context: A standard dictionary for contextual values
92 if hasattr(self, '_columns'):
93 if self._columns.get('date_closed'):
97 if self._columns.get('date_open'):
101 return super(osv.osv, self).copy(cr, uid, id, default, context=context)
103 def _get_default_email(self, cr, uid, context):
104 """Gives default email address for current user
105 @param self: The object pointer
106 @param cr: the current row, from the database cursor,
107 @param uid: the current user’s ID for security checks,
108 @param context: A standard dictionary for contextual values
110 if not context.get('portal', False):
112 user = self.pool.get('res.users').browse(cr, uid, uid, context)
113 if not user.address_id:
115 return user.address_id.email
117 def _get_default_user(self, cr, uid, context):
118 """Gives current user id
119 @param self: The object pointer
120 @param cr: the current row, from the database cursor,
121 @param uid: the current user’s ID for security checks,
122 @param context: A standard dictionary for contextual values
124 if context.get('portal', False):
128 def _get_section(self, cr, uid, context):
129 """Gives section id for current User
130 @param self: The object pointer
131 @param cr: the current row, from the database cursor,
132 @param uid: the current user’s ID for security checks,
133 @param context: A standard dictionary for contextual values
135 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
136 return user.context_section_id.id or False
138 def stage_next(self, cr, uid, ids, context=None):
139 """This function computes next stage for case from its current stage
140 using available stage for that case type
141 @param self: The object pointer
142 @param cr: the current row, from the database cursor,
143 @param uid: the current user’s ID for security checks,
144 @param ids: List of case IDs
145 @param context: A standard dictionary for contextual values"""
148 s = self.get_stage_dict(cr, uid, ids, context=context)
151 for case in self.browse(cr, uid, ids, context):
153 st = not context.get('force_domain', False) and case.stage_id.id or False
155 data = {'stage_id': s[section][st]}
156 stage = s[section][st]
157 self.write(cr, uid, [case.id], data)
160 def get_stage_dict(self, cr, uid, ids, context=None):
161 """This function gives dictionary for stage according to stage levels
162 @param self: The object pointer
163 @param cr: the current row, from the database cursor,
164 @param uid: the current user’s ID for security checks,
165 @param ids: List of case IDs
166 @param context: A standard dictionary for contextual values"""
169 stage_obj = self.pool.get('crm.case.stage')
170 res = self.read(cr, uid, ids, ['section_id', 'stage_id'], context)[0]
171 section_id = res['section_id'] and res['section_id'][0] or False
172 stage_id = res['stage_id'] and res['stage_id'][0] or False
174 # We select either the stages in the same section as the current stage
175 # if it a stage that does not have a section, or the stages of the
176 # current section of the case
178 stage_record = stage_obj.browse(cr, uid, stage_id)
179 if not stage_record.section_id:
180 section_id = False # only select stages without section
182 domain = [('object_id.model', '=', self._name), ('section_id', '=', section_id)]
183 if 'force_domain' in context and context['force_domain']:
184 domain += context['force_domain']
185 sid = stage_obj.search(cr, uid, domain, context=context)
190 for stage in stage_obj.browse(cr, uid, sid, context=context):
191 s.setdefault(section, {})
192 s[section][previous.get(section, False)] = stage.id
193 previous[section] = stage.id
196 def stage_previous(self, cr, uid, ids, context=None):
197 """This function computes previous stage for case from its current stage
198 using available stage for that case type
199 @param self: The object pointer
200 @param cr: the current row, from the database cursor,
201 @param uid: the current user’s ID for security checks,
202 @param ids: List of case IDs
203 @param context: A standard dictionary for contextual values"""
207 s = self.get_stage_dict(cr, uid, ids, context=context)
209 stage_pool = self.pool.get('crm.case.stage')
210 for case in self.browse(cr, uid, ids, context):
212 st = not context.get('force_domain', False) and case.stage_id.id or False
213 s[section] = dict([(v, k) for (k, v) in s[section].iteritems()])
215 data = {'stage_id': s[section][st]}
217 stage = stage_pool.browse(cr, uid, s[section][st], context=context)
219 data.update({'probability': stage.probability})
220 self.write(cr, uid, [case.id], data)
223 def onchange_partner_id(self, cr, uid, ids, part, email=False):
224 """This function returns value of partner address based on partner
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 part: Partner's id
230 @email: Partner's email ID
233 return {'value': {'partner_address_id': False,
237 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
238 data = {'partner_address_id': addr['contact']}
239 data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
240 return {'value': data}
242 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
243 """This function returns value of partner email based on Partner Address
244 @param self: The object pointer
245 @param cr: the current row, from the database cursor,
246 @param uid: the current user’s ID for security checks,
247 @param ids: List of case IDs
248 @param add: Id of Partner's address
249 @email: Partner's email ID
252 return {'value': {'email_from': False}}
253 address = self.pool.get('res.partner.address').browse(cr, uid, add)
254 return {'value': {'email_from': address.email, 'phone': address.phone}}
256 def _history(self, cr, uid, cases, keyword, history=False, subject=None, email=False, details=None, email_from=False, message_id=False, attach=[], context={}):
257 mailgate_pool = self.pool.get('mailgate.thread')
258 return mailgate_pool.history(cr, uid, cases, keyword, history=history,\
259 subject=subject, email=email, \
260 details=details, email_from=email_from,\
261 message_id=message_id, attach=attach, \
264 def case_open(self, cr, uid, ids, *args):
266 @param self: The object pointer
267 @param cr: the current row, from the database cursor,
268 @param uid: the current user’s ID for security checks,
269 @param ids: List of case Ids
270 @param *args: Tuple Value for additional Params
272 cases = self.browse(cr, uid, ids)
273 self._history(cr, uid, cases, _('Open'))
275 data = {'state': 'open', 'active': True}
277 data['user_id'] = uid
278 self.write(cr, uid, case.id, data)
279 self._action(cr, uid, cases, 'open')
282 def case_close(self, cr, uid, ids, *args):
284 @param self: The object pointer
285 @param cr: the current row, from the database cursor,
286 @param uid: the current user’s ID for security checks,
287 @param ids: List of case Ids
288 @param *args: Tuple Value for additional Params
290 cases = self.browse(cr, uid, ids)
291 cases[0].state # to fill the browse record cache
292 self._history(cr, uid, cases, _('Close'))
293 self.write(cr, uid, ids, {'state': 'done',
294 'date_closed': time.strftime('%Y-%m-%d %H:%M:%S'),
297 # We use the cache of cases to keep the old case state
299 self._action(cr, uid, cases, 'done')
302 def case_escalate(self, cr, uid, ids, *args):
303 """Escalates case to top level
304 @param self: The object pointer
305 @param cr: the current row, from the database cursor,
306 @param uid: the current user’s ID for security checks,
307 @param ids: List of case Ids
308 @param *args: Tuple Value for additional Params
310 cases = self.browse(cr, uid, ids)
312 data = {'active': True}
314 if case.section_id.parent_id:
315 data['section_id'] = case.section_id.parent_id.id
316 if case.section_id.parent_id.change_responsible:
317 if case.section_id.parent_id.user_id:
318 data['user_id'] = case.section_id.parent_id.user_id.id
320 raise osv.except_osv(_('Error !'), _('You can not escalate, You are already at the top level regarding your sales-team category.'))
321 self.write(cr, uid, [case.id], data)
322 cases = self.browse(cr, uid, ids)
323 self._history(cr, uid, cases, _('Escalate'))
324 self._action(cr, uid, cases, 'escalate')
327 def case_cancel(self, cr, uid, ids, *args):
329 @param self: The object pointer
330 @param cr: the current row, from the database cursor,
331 @param uid: the current user’s ID for security checks,
332 @param ids: List of case Ids
333 @param *args: Tuple Value for additional Params
335 cases = self.browse(cr, uid, ids)
336 cases[0].state # to fill the browse record cache
337 self._history(cr, uid, cases, _('Cancel'))
338 self.write(cr, uid, ids, {'state': 'cancel',
340 self._action(cr, uid, cases, 'cancel')
342 message = "The " + self._description + " '" + case.name + "' has been Cancelled."
343 #TODO: Need to differentiate lead and opportunity
344 # if hasattr(case, 'type'):
345 # #TO CHECK: hasattr gives warning for other crm objects that don't have field 'type'
346 # message = "The " + (case.type or 'Case').title() + " '" + case.name + "' has been Cancelled."
347 self.log(cr, uid, case.id, message)
350 def case_pending(self, cr, uid, ids, *args):
351 """Marks case as pending
352 @param self: The object pointer
353 @param cr: the current row, from the database cursor,
354 @param uid: the current user’s ID for security checks,
355 @param ids: List of case Ids
356 @param *args: Tuple Value for additional Params
358 cases = self.browse(cr, uid, ids)
359 cases[0].state # to fill the browse record cache
360 self._history(cr, uid, cases, _('Pending'))
361 self.write(cr, uid, ids, {'state': 'pending', 'active': True})
362 self._action(cr, uid, cases, 'pending')
365 def case_reset(self, cr, uid, ids, *args):
366 """Resets case as draft
367 @param self: The object pointer
368 @param cr: the current row, from the database cursor,
369 @param uid: the current user’s ID for security checks,
370 @param ids: List of case Ids
371 @param *args: Tuple Value for additional Params
373 cases = self.browse(cr, uid, ids)
374 cases[0].state # to fill the browse record cache
375 self._history(cr, uid, cases, _('Draft'))
376 self.write(cr, uid, ids, {'state': 'draft', 'active': True})
377 self._action(cr, uid, cases, 'draft')
380 def remind_partner(self, cr, uid, ids, context={}, attach=False):
383 @param self: The object pointer
384 @param cr: the current row, from the database cursor,
385 @param uid: the current user’s ID for security checks,
386 @param ids: List of Remind Partner's IDs
387 @param context: A standard dictionary for contextual values
390 return self.remind_user(cr, uid, ids, context, attach,
393 def remind_user(self, cr, uid, ids, context={}, attach=False,destination=True):
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 Remind user's IDs
399 @param context: A standard dictionary for contextual values
402 for case in self.browse(cr, uid, ids):
403 if not case.section_id.reply_to:
404 raise osv.except_osv(_('Error!'), ("Reply To is not specified in the sales team"))
405 if not case.email_from:
406 raise osv.except_osv(_('Error!'), ("Partner Email is not specified in Case"))
407 if case.section_id.reply_to and case.email_from:
408 src = case.email_from
409 dest = case.section_id.reply_to
410 body = case.description or ""
412 body = case.message_ids[0].description or ""
414 src, dest = dest, src
415 if body and case.user_id.signature:
417 body += '\n\n%s' % (case.user_id.signature)
419 body = '\n\n%s' % (case.user_id.signature)
421 body = self.format_body(body)
423 attach_to_send = None
426 attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
427 attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname','datas'])
428 attach_to_send = map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send)
431 subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
437 reply_to=case.section_id.reply_to,
438 openobject_id=str(case.id),
439 attach=attach_to_send
441 self._history(cr, uid, [case], _('Send'), history=True, subject=subject, email=dest, details=body, email_from=src)
444 def _check(self, cr, uid, ids=False, context={}):
446 Function called by the scheduler to process cases for date actions
447 Only works on not done and cancelled cases
449 @param self: The object pointer
450 @param cr: the current row, from the database cursor,
451 @param uid: the current user’s ID for security checks,
452 @param context: A standard dictionary for contextual values
454 cr.execute('select * from crm_case \
455 where (date_action_last<%s or date_action_last is null) \
456 and (date_action_next<=%s or date_action_next is null) \
457 and state not in (\'cancel\',\'done\')',
458 (time.strftime("%Y-%m-%d %H:%M:%S"),
459 time.strftime('%Y-%m-%d %H:%M:%S')))
461 ids2 = map(lambda x: x[0], cr.fetchall() or [])
462 cases = self.browse(cr, uid, ids2, context)
463 return self._action(cr, uid, cases, False, context=context)
465 def _action(self, cr, uid, cases, state_to, scrit=None, context={}):
468 context['state_to'] = state_to
469 rule_obj = self.pool.get('base.action.rule')
470 model_obj = self.pool.get('ir.model')
471 model_ids = model_obj.search(cr, uid, [('model','=',self._name)])
472 rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])])
473 return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
475 def format_body(self, body):
476 return self.pool.get('base.action.rule').format_body(body)
478 def format_mail(self, obj, body):
479 return self.pool.get('base.action.rule').format_mail(obj, body)
481 def message_followers(self, cr, uid, ids, context=None):
482 """ Get a list of emails of the people following this thread
485 for case in self.browse(cr, uid, ids, context=context):
488 l.append(case.email_cc)
489 if case.user_id and case.user_id.user_email:
490 l.append(case.user_id.user_email)
495 class crm_case_section(osv.osv):
498 _name = "crm.case.section"
499 _description = "Sales Teams"
500 _order = "complete_name"
502 def get_full_name(self, cr, uid, ids, field_name, arg, context={}):
503 return dict(self.name_get(cr, uid, ids, context))
506 'name': fields.char('Sales Team', size=64, required=True, translate=True),
507 'complete_name': fields.function(get_full_name, method=True, type='char', size=256, readonly=True, store=True),
508 'code': fields.char('Code', size=8),
509 'active': fields.boolean('Active', help="If the active field is set to "\
510 "true, it will allow you to hide the sales team without removing it."),
511 'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
512 '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"),
513 'user_id': fields.many2one('res.users', 'Responsible User'),
514 'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'),
515 'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by OpenERP about cases in this sales team"),
516 'parent_id': fields.many2one('crm.case.section', 'Parent Team'),
517 'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'),
518 'resource_calendar_id': fields.many2one('resource.calendar', "Resource's Calendar"),
519 'note': fields.text('Description'),
520 'working_hours': fields.float('Working Hours', digits=(16,2 )),
524 'active': lambda *a: 1,
525 'allow_unlink': lambda *a: 1,
529 ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !')
532 def _check_recursion(self, cr, uid, ids):
535 Checks for recursion level for sales team
536 @param self: The object pointer
537 @param cr: the current row, from the database cursor,
538 @param uid: the current user’s ID for security checks,
539 @param ids: List of Sales team ids
544 cr.execute('select distinct parent_id from crm_case_section where id IN %s', (tuple(ids),))
545 ids = filter(None, map(lambda x: x[0], cr.fetchall()))
553 (_check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id'])
556 def name_get(self, cr, uid, ids, context=None):
557 """Overrides orm name_get method
558 @param self: The object pointer
559 @param cr: the current row, from the database cursor,
560 @param uid: the current user’s ID for security checks,
561 @param ids: List of sales team ids
569 reads = self.read(cr, uid, ids, ['name', 'parent_id'], context)
572 name = record['name']
573 if record['parent_id']:
574 name = record['parent_id'][1] + ' / ' + name
575 res.append((record['id'], name))
581 class crm_case_categ(osv.osv):
582 """ Category of Case """
584 _name = "crm.case.categ"
585 _description = "Category of case"
588 'name': fields.char('Name', size=64, required=True, translate=True),
589 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
590 'object_id': fields.many2one('ir.model', 'Object Name'),
593 def _find_object_id(self, cr, uid, context=None):
594 """Finds id for case object
595 @param self: The object pointer
596 @param cr: the current row, from the database cursor,
597 @param uid: the current user’s ID for security checks,
598 @param context: A standard dictionary for contextual values
601 object_id = context and context.get('object_id', False) or False
602 ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
603 return ids and ids[0]
606 'object_id' : _find_object_id
612 class crm_case_resource_type(osv.osv):
613 """ Resource Type of case """
614 _name = "crm.case.resource.type"
615 _description = "Campaign"
618 'name': fields.char('Campaign Name', size=64, required=True, translate=True),
619 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
621 crm_case_resource_type()
624 class crm_case_stage(osv.osv):
625 """ Stage of case """
627 _name = "crm.case.stage"
628 _description = "Stage of case"
633 'name': fields.char('Stage Name', size=64, required=True, translate=True),
634 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
635 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of case stages."),
636 'object_id': fields.many2one('ir.model', 'Object Name'),
637 'probability': fields.float('Probability (%)', required=True, help="This percentage depicts the default/average probability of the Case for this stage to be a success"),
638 'on_change': fields.boolean('Change Probability Automatically', \
639 help="Change Probability on next and previous stages."),
640 'requirements': fields.text('Requirements')
642 def _find_object_id(self, cr, uid, context=None):
643 """Finds id for case object
644 @param self: The object pointer
645 @param cr: the current row, from the database cursor,
646 @param uid: the current user’s ID for security checks,
647 @param context: A standard dictionary for contextual values
649 object_id = context and context.get('object_id', False) or False
650 ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)])
651 return ids and ids[0]
654 'sequence': lambda *args: 1,
655 'probability': lambda *args: 0.0,
656 'object_id' : _find_object_id
661 def _links_get(self, cr, uid, context=None):
662 """Gets links value for reference field
663 @param self: The object pointer
664 @param cr: the current row, from the database cursor,
665 @param uid: the current user’s ID for security checks,
666 @param context: A standard dictionary for contextual values
670 obj = self.pool.get('res.request.link')
671 ids = obj.search(cr, uid, [])
672 res = obj.read(cr, uid, ids, ['object', 'name'], context)
673 return [(r['object'], r['name']) for r in res]
675 class users(osv.osv):
676 _inherit = 'res.users'
677 _description = "Users"
679 'context_section_id': fields.many2one('crm.case.section', 'Sales Team'),
684 class res_partner(osv.osv):
685 _inherit = 'res.partner'
687 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
692 class crm_case_section_custom(osv.osv):
693 _name = "crm.case.section.custom"
694 _description = 'Custom CRM Case Teams'
696 'name': fields.char('Case Team',size=64, required=True, translate=True),
697 'code': fields.char('Team Code',size=8),
698 'active': fields.boolean('Active'),
699 'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
700 'sequence': fields.integer('Sequence'),
701 'user_id': fields.many2one('res.users', 'Responsible User'),
702 'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by OpenERP about cases in this section"),
703 'parent_id': fields.many2one('crm.case.section.custom', 'Parent Team'),
704 'note': fields.text('Notes'),
713 ('code_uniq', 'unique (code)', 'The code of the team must be unique !')
716 def _check_recursion(self, cr, uid, ids):
719 cr.execute('SELECT DISTINCT parent_id FROM crm_case_section_custom '\
722 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
728 (_check_recursion, 'Error ! You cannot create recursive sections.', ['parent_id'])
731 crm_case_section_custom()
734 class crm_case_custom(osv.osv, crm_case):
735 _name = 'crm.case.custom'
736 _inherit = 'mailgate.thread'
737 _description = "Custom CRM Case"
740 'id': fields.integer('ID', readonly=True),
741 'name': fields.char('Name',size=64,required=True),
742 'priority': fields.selection(AVAILABLE_PRIORITIES, 'Priority'),
743 'active': fields.boolean('Active'),
744 'description': fields.text('Description'),
745 'section_id': fields.many2one('crm.case.section.custom', 'Team', required=True, select=True),
746 'probability': fields.float('Probability (%)'),
747 'email_from': fields.char('Partner Email', size=128),
748 'email_cc': fields.char('CC', size=252),
749 'partner_id': fields.many2one('res.partner', 'Partner'),
750 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"),
751 'date': fields.datetime('Date'),
752 'create_date': fields.datetime('Created' ,readonly=True),
753 'date_deadline': fields.datetime('Deadline'),
754 'date_closed': fields.datetime('Closed', readonly=True),
755 'user_id': fields.many2one('res.users', 'Responsible'),
756 'state': fields.selection(AVAILABLE_STATES, 'Status', size=16, readonly=True),
757 'ref' : fields.reference('Reference', selection=_links_get, size=128),
758 'date_action_last': fields.datetime('Last Action', readonly=1),
759 'date_action_next': fields.datetime('Next Action', readonly=1),
765 'priority': AVAILABLE_PRIORITIES[2][0],
766 'date': time.strftime('%Y-%m-%d %H:%M:%S'),
772 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: