1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
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 ##############################################################################
26 from osv import fields
28 from tools.translate import _
33 ('cancel', 'Cancelled'),
34 ('open', 'In Progress'),
35 ('pending', 'Pending'),
39 AVAILABLE_PRIORITIES = [
47 class crm_case_channel(osv.osv):
48 _name = "crm.case.channel"
49 _description = "Channels"
52 'name': fields.char('Channel Name', size=64, required=True),
53 'active': fields.boolean('Active'),
56 'active': lambda *a: 1,
59 class crm_case_stage(osv.osv):
60 """ Model for case stages. This models the main stages of a document
61 management flow. Main CRM objects (leads, opportunities, project
62 issues, ...) will now use only stages, instead of state and stages.
63 Stages are for example used to display the kanban view of records.
65 _name = "crm.case.stage"
66 _description = "Stage of case"
71 'name': fields.char('Stage Name', size=64, required=True, translate=True),
72 'sequence': fields.integer('Sequence', help="Used to order stages. Lower is better."),
73 'probability': fields.float('Probability (%)', required=True, help="This percentage depicts the default/average probability of the Case for this stage to be a success"),
74 'on_change': fields.boolean('Change Probability Automatically', help="Setting this stage will change the probability automatically on the opportunity."),
75 'requirements': fields.text('Requirements'),
76 'section_ids':fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', string='Sections',
77 help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
78 'state': fields.selection(AVAILABLE_STATES, 'State', required=True, help="The related state for the stage. The state of your document will automatically change regarding the selected stage. For example, if a stage is related to the state 'Close', when your document reaches this stage, it will be automatically have the 'closed' state."),
79 'case_default': fields.boolean('Common to All Teams', help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
80 'fold': fields.boolean('Hide in Views when Empty', help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
84 'sequence': lambda *args: 1,
85 'probability': lambda *args: 0.0,
90 class crm_case_section(osv.osv):
91 """ Model for sales teams. """
92 _name = "crm.case.section"
93 _description = "Sales Teams"
94 _order = "complete_name"
96 def get_full_name(self, cr, uid, ids, field_name, arg, context=None):
97 return dict(self.name_get(cr, uid, ids, context=context))
100 'name': fields.char('Sales Team', size=64, required=True, translate=True),
101 'complete_name': fields.function(get_full_name, type='char', size=256, readonly=True, store=True),
102 'code': fields.char('Code', size=8),
103 'active': fields.boolean('Active', help="If the active field is set to "\
104 "true, it will allow you to hide the sales team without removing it."),
105 'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
106 'change_responsible': fields.boolean('Reassign Escalated', help="When escalating to this team override the saleman with the team leader."),
107 'user_id': fields.many2one('res.users', 'Team Leader'),
108 'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'),
109 '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"),
110 'parent_id': fields.many2one('crm.case.section', 'Parent Team'),
111 'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'),
112 'resource_calendar_id': fields.many2one('resource.calendar', "Working Time", help="Used to compute open days"),
113 'note': fields.text('Description'),
114 'working_hours': fields.float('Working Hours', digits=(16,2 )),
115 'stage_ids': fields.many2many('crm.case.stage', 'section_stage_rel', 'section_id', 'stage_id', 'Stages'),
118 def _get_stage_common(self, cr, uid, context):
119 ids = self.pool.get('crm.case.stage').search(cr, uid, [('case_default','=',1)], context=context)
123 'active': lambda *a: 1,
124 'allow_unlink': lambda *a: 1,
125 'stage_ids': _get_stage_common
129 ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !')
133 (osv.osv._check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id'])
136 def name_get(self, cr, uid, ids, context=None):
137 """Overrides orm name_get method"""
138 if not isinstance(ids, list) :
143 reads = self.read(cr, uid, ids, ['name', 'parent_id'], context)
146 name = record['name']
147 if record['parent_id']:
148 name = record['parent_id'][1] + ' / ' + name
149 res.append((record['id'], name))
152 class crm_case_categ(osv.osv):
153 """ Category of Case """
154 _name = "crm.case.categ"
155 _description = "Category of Case"
157 'name': fields.char('Name', size=64, required=True, translate=True),
158 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
159 'object_id': fields.many2one('ir.model', 'Object Name'),
162 def _find_object_id(self, cr, uid, context=None):
163 """Finds id for case object"""
164 object_id = context and context.get('object_id', False) or False
165 ids = self.pool.get('ir.model').search(cr, uid, [('id', '=', object_id)])
166 return ids and ids[0] or False
169 'object_id' : _find_object_id
172 class crm_case_resource_type(osv.osv):
173 """ Resource Type of case """
174 _name = "crm.case.resource.type"
175 _description = "Campaign"
178 'name': fields.char('Campaign Name', size=64, required=True, translate=True),
179 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
182 class crm_base(object):
183 """ Base utility mixin class for objects willing to manage their state.
184 Object subclassing this class should define the following colums:
185 - ``date_open`` (datetime field)
186 - ``date_closed`` (datetime field)
187 - ``user_id`` (many2one to res.users)
188 - ``partner_id`` (many2one to res.partner)
189 - ``state`` (selection field)
192 def case_open(self, cr, uid, ids, context=None):
194 cases = self.browse(cr, uid, ids, context=context)
196 values = {'active': True}
197 if case.state == 'draft':
198 values['date_open'] = fields.datetime.now()
200 values['user_id'] = uid
201 self.case_set(cr, uid, [case.id], 'open', values, context=context)
202 self.case_open_send_note(cr, uid, [case.id], context=context)
205 def case_close(self, cr, uid, ids, context=None):
207 self.case_set(cr, uid, ids, 'done', {'date_closed': fields.datetime.now()}, context=context)
208 self.case_close_send_note(cr, uid, ids, context=context)
211 def case_cancel(self, cr, uid, ids, context=None):
213 self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
214 self.case_cancel_send_note(cr, uid, ids, context=context)
217 def case_pending(self, cr, uid, ids, context=None):
218 """ Sets case as pending """
219 self.case_set(cr, uid, ids, 'pending', {'active': True}, context=context)
220 self.case_pending_send_note(cr, uid, ids, context=context)
223 def case_reset(self, cr, uid, ids, context=None):
224 """ Resets case as draft """
225 self.case_set(cr, uid, ids, 'draft', {'active': True}, context=context)
226 self.case_close_send_note(cr, uid, ids, context=context)
229 def case_set(self, cr, uid, ids, state_name, update_values=None, context=None):
230 """ Generic method for setting case. This methods wraps the update
231 of the record, as well as call to _action and browse record
234 :params: state_name: the new value of the state, such as
236 :params: update_values: values that will be added with the state
237 update when writing values to the record.
239 cases = self.browse(cr, uid, ids, context=context)
240 cases[0].state # fill browse record cache, for _action having old and new values
241 if update_values is None:
243 update_values.update({'state': state_name})
244 self.write(cr, uid, ids, update_values, context=context)
245 self._action(cr, uid, cases, state_name, context=context)
247 def _action(self, cr, uid, cases, state_to, scrit=None, context=None):
250 context['state_to'] = state_to
251 rule_obj = self.pool.get('base.action.rule')
252 model_obj = self.pool.get('ir.model')
253 model_ids = model_obj.search(cr, uid, [('model','=',self._name)])
254 rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])])
255 return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
257 # ******************************
259 # ******************************
261 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
264 def case_open_send_note(self, cr, uid, ids, context=None):
266 msg = _('%s has been <b>opened</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
267 self.message_append_note(cr, uid, [id], body=msg, context=context)
270 def case_close_send_note(self, cr, uid, ids, context=None):
272 msg = _('%s has been <b>closed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
273 self.message_append_note(cr, uid, [id], body=msg, context=context)
276 def case_cancel_send_note(self, cr, uid, ids, context=None):
278 msg = _('%s has been <b>canceled</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
279 self.message_append_note(cr, uid, [id], body=msg, context=context)
282 def case_pending_send_note(self, cr, uid, ids, context=None):
284 msg = _('%s is now <b>pending</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
285 self.message_append_note(cr, uid, [id], body=msg, context=context)
288 def case_reset_send_note(self, cr, uid, ids, context=None):
290 msg = _('%s has been <b>renewed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
291 self.message_append_note(cr, uid, [id], body=msg, context=context)
294 class crm_case(object):
295 """ Base utility mixin class for objects willing to manage their stages.
296 Object that inherit from this class should inherit from mailgate.thread
297 to have access to the mail gateway, as well as Chatter. Objects
298 subclassing this class should define the following colums:
299 - ``date_open`` (datetime field)
300 - ``date_closed`` (datetime field)
301 - ``user_id`` (many2one to res.users)
302 - ``partner_id`` (many2one to res.partner)
303 - ``stage_id`` (many2one to a stage definition model)
304 - ``state`` (selection field, related to the stage_id.state)
307 def _get_default_partner(self, cr, uid, context=None):
308 """ Gives id of partner for current user
309 :param context: if portal in context is false return false anyway
313 if not context.get('portal', False):
315 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
316 if hasattr(user, 'partner_address_id') and user.partner_address_id:
317 return user.partner_address_id
318 return user.company_id.partner_id.id
320 def _get_default_email(self, cr, uid, context=None):
321 """ Gives default email address for current user
322 :param context: if portal in context is false return false anyway
324 if not context.get('portal', False):
326 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
327 return user.user_email
329 def _get_default_user(self, cr, uid, context=None):
330 """ Gives current user id
331 :param context: if portal in context is false return false anyway
333 if context and context.get('portal', False):
337 def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
338 """ This function returns value of partner email based on Partner Address
339 :param add: Id of Partner's address
340 :param email: Partner's email ID
342 data = {'value': {'email_from': False, 'phone':False}}
344 address = self.pool.get('res.partner').browse(cr, uid, add)
345 data['value'] = {'email_from': address and address.email or False ,
346 'phone': address and address.phone or False}
347 if 'phone' not in self._columns:
348 del data['value']['phone']
351 def onchange_partner_id(self, cr, uid, ids, part, email=False):
352 """ This function returns value of partner address based on partner
353 :param part: Partner's id
354 :param email: Partner's email ID
358 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
359 data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
360 return {'value': data}
362 def _get_default_section(self, cr, uid, context=None):
363 """ Gives default section by checking if present in the context """
366 if context.get('portal', False):
368 if type(context.get('default_section_id')) in (int, long):
369 return context.get('default_section_id')
370 if isinstance(context.get('default_section_id'), basestring):
371 section_name = context['default_section_id']
372 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
373 if len(section_ids) == 1:
374 return section_ids[0][0]
377 def _get_default_stage_id(self, cr, uid, context=None):
378 """ Gives default stage_id """
379 section_id = self._get_default_section(cr, uid, context=context)
380 return self.stage_find(cr, uid, section_id, [('state', '=', 'draft')], context=context)
382 def stage_find(self, cr, uid, section_id, domain=[], order='sequence', context=None):
383 """ Find stage, within a sales team, with a domain on the search,
384 ordered by the order parameter. If several stages match the
385 search criterions, the first one will be returned, according
386 to the requested search order.
387 :param section_id: if set, the search is limited to stages that
388 belongs to the given sales team, or that are
389 global (case_default flag set to True)
390 :param domain: a domain on the search of stages
391 :param order: order of the search
393 domain = list(domain)
395 domain += ['|', ('section_ids', '=', section_id), ('case_default', '=', True)]
396 stage_ids = self.pool.get('crm.case.stage').search(cr, uid, domain, order=order, context=context)
401 def stage_set_with_state_name(self, cr, uid, cases, state_name, context=None):
402 """ Set a new stage, with a state_name instead of a stage_id
403 :param cases: browse_record of cases
405 if isinstance(cases, (int, long)):
406 cases = self.browse(cr, uid, cases, context=context)
408 section_id = case.section_id.id if case.section_id else None
409 stage_id = self.stage_find(cr, uid, section_id, [('state', '=', state_name)], context=context)
411 self.stage_set(cr, uid, [case.id], stage_id, context=context)
414 def stage_set(self, cr, uid, ids, stage_id, context=None):
416 if hasattr(self,'onchange_stage_id'):
417 value = self.onchange_stage_id(cr, uid, ids, stage_id)['value']
418 value['stage_id'] = stage_id
419 return self.write(cr, uid, ids, value, context=context)
421 def stage_change(self, cr, uid, ids, op, order, context=None):
422 for case in self.browse(cr, uid, ids, context=context):
425 seq = case.stage_id.sequence
428 section_id = case.section_id.id
429 next_stage_id = self.stage_find(cr, uid, section_id, [('sequence',op,seq)],order)
431 return self.stage_set(cr, uid, [case.id], next_stage_id, context=context)
434 def stage_next(self, cr, uid, ids, context=None):
435 """ This function computes next stage for case from its current stage
436 using available stage for that case type
438 return self.stage_change(cr, uid, ids, '>','sequence', context)
440 def stage_previous(self, cr, uid, ids, context=None):
441 """ This function computes previous stage for case from its current
442 stage using available stage for that case type
444 return self.stage_change(cr, uid, ids, '<', 'sequence desc', context)
446 def copy(self, cr, uid, id, default=None, context=None):
447 """ Overrides orm copy method to avoid copying messages,
448 as well as date_closed and date_open columns if they
453 if hasattr(self, '_columns'):
454 if self._columns.get('date_closed'):
455 default.update({ 'date_closed': False, })
456 if self._columns.get('date_open'):
457 default.update({ 'date_open': False })
458 return super(crm_case, self).copy(cr, uid, id, default, context=context)
460 def case_escalate(self, cr, uid, ids, context=None):
461 """ Escalates case to parent level """
462 cases = self.browse(cr, uid, ids, context=context)
463 cases[0].state # fill browse record cache, for _action having old and new values
465 data = {'active': True}
466 if case.section_id.parent_id:
467 data['section_id'] = case.section_id.parent_id.id
468 if case.section_id.parent_id.change_responsible:
469 if case.section_id.parent_id.user_id:
470 data['user_id'] = case.section_id.parent_id.user_id.id
472 raise osv.except_osv(_('Error !'), _('You can not escalate, you are already at the top level regarding your sales-team category.'))
473 self.write(cr, uid, [case.id], data)
474 case.case_escalate_send_note(case.section_id.parent_id, context=context)
475 cases = self.browse(cr, uid, ids, context=context)
476 self._action(cr, uid, cases, 'escalate', context=context)
479 def case_open(self, cr, uid, ids, context=None):
481 cases = self.browse(cr, uid, ids, context=context)
483 data = {'active': True}
484 if case.stage_id and case.stage_id.state == 'draft':
485 data['date_open'] = fields.datetime.now()
487 data['user_id'] = uid
488 self.case_set(cr, uid, [case.id], 'open', data, context=context)
489 self.case_open_send_note(cr, uid, [case.id], context=context)
492 def case_close(self, cr, uid, ids, context=None):
494 self.case_set(cr, uid, ids, 'done', {'active': True, 'date_closed': fields.datetime.now()}, context=context)
495 self.case_close_send_note(cr, uid, ids, context=context)
498 def case_cancel(self, cr, uid, ids, context=None):
500 self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
501 self.case_cancel_send_note(cr, uid, ids, context=context)
504 def case_pending(self, cr, uid, ids, context=None):
505 """ Set case as pending """
506 self.case_set(cr, uid, ids, 'pending', {'active': True}, context=context)
507 self.case_pending_send_note(cr, uid, ids, context=context)
510 def case_reset(self, cr, uid, ids, context=None):
511 """ Resets case as draft """
512 self.case_set(cr, uid, ids, 'draft', {'active': True}, context=context)
513 self.case_reset_send_note(cr, uid, ids, context=context)
516 def case_set(self, cr, uid, ids, new_state_name=None, values_to_update=None, new_stage_id=None, context=None):
518 cases = self.browse(cr, uid, ids, context=context)
519 cases[0].state # fill browse record cache, for _action having old and new values
520 # 1. update the stage
522 self.stage_set_with_state_name(cr, uid, cases, new_state_name, context=context)
523 elif not (new_stage_id is None):
524 self.stage_set(cr, uid, ids, new_stage_id, context=context)
527 self.write(cr, uid, ids, values_to_update, context=context)
528 # 3. call _action for base action rule
530 self._action(cr, uid, cases, new_state_name, context=context)
531 elif not (new_stage_id is None):
532 stage = self.pool.get('crm.case.stage').browse(cr, uid, [new_stage_id], context=context)[0]
533 new_state_name = stage.state
534 self._action(cr, uid, cases, new_state_name, context=context)
537 def remind_partner(self, cr, uid, ids, context=None, attach=False):
538 return self.remind_user(cr, uid, ids, context, attach,
541 def remind_user(self, cr, uid, ids, context=None, attach=False, destination=True):
542 mail_message = self.pool.get('mail.message')
543 for case in self.browse(cr, uid, ids, context=context):
544 if not destination and not case.email_from:
546 if not case.user_id.user_email:
548 if destination and case.section_id.user_id:
549 case_email = case.section_id.user_id.user_email
551 case_email = case.user_id.user_email
554 dest = case.user_id.user_email or ""
555 body = case.description or ""
556 for message in case.message_ids:
557 if message.email_from and message.body_text:
558 body = message.body_text
562 src, dest = dest, case.email_from
563 if body and case.user_id.signature:
565 body += '\n\n%s' % (case.user_id.signature)
567 body = '\n\n%s' % (case.user_id.signature)
569 body = self.format_body(body)
574 attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
575 attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname', 'datas'])
576 attach_to_send = dict(map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send))
579 subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
580 mail_message.schedule_with_attach(cr, uid,
586 reply_to=case.section_id.reply_to,
588 attachments=attach_to_send,
593 def _check(self, cr, uid, ids=False, context=None):
594 """Function called by the scheduler to process cases for date actions
595 Only works on not done and cancelled cases
597 cr.execute('select * from crm_case \
598 where (date_action_last<%s or date_action_last is null) \
599 and (date_action_next<=%s or date_action_next is null) \
600 and state not in (\'cancel\',\'done\')',
601 (time.strftime("%Y-%m-%d %H:%M:%S"),
602 time.strftime('%Y-%m-%d %H:%M:%S')))
604 ids2 = map(lambda x: x[0], cr.fetchall() or [])
605 cases = self.browse(cr, uid, ids2, context=context)
606 return self._action(cr, uid, cases, False, context=context)
608 def format_body(self, body):
609 return self.pool.get('base.action.rule').format_body(body)
611 def format_mail(self, obj, body):
612 return self.pool.get('base.action.rule').format_mail(obj, body)
614 def message_thread_followers(self, cr, uid, ids, context=None):
616 for case in self.browse(cr, uid, ids, context=context):
619 l.append(case.email_cc)
620 if case.user_id and case.user_id.user_email:
621 l.append(case.user_id.user_email)
625 # ******************************
627 # ******************************
629 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
632 def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
635 msg = '%s has been <b>escalated</b> to <b>%s</b>.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context), new_section.name)
637 msg = '%s has been <b>escalated</b>.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
638 self.message_append_note(cr, uid, [id], 'System Notification', msg, context=context)
641 def _links_get(self, cr, uid, context=None):
642 """Gets links value for reference field"""
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 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: