ec7fcc3388a13ddd936da9c8b3c628d795101df3
[odoo/odoo.git] / addons / crm / crm.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23 import base64
24 import tools
25
26 from osv import fields
27 from osv import osv
28 from tools.translate import _
29
30 MAX_LEVEL = 15
31 AVAILABLE_STATES = [
32     ('draft', 'New'),
33     ('cancel', 'Cancelled'),
34     ('open', 'In Progress'),
35     ('pending', 'Pending'),
36     ('done', 'Closed')
37 ]
38
39 AVAILABLE_PRIORITIES = [
40     ('1', 'Highest'),
41     ('2', 'High'),
42     ('3', 'Normal'),
43     ('4', 'Low'),
44     ('5', 'Lowest'),
45 ]
46
47 class crm_case_channel(osv.osv):
48     _name = "crm.case.channel"
49     _description = "Channels"
50     _order = 'name'
51     _columns = {
52         'name': fields.char('Channel Name', size=64, required=True),
53         'active': fields.boolean('Active'),
54     }
55     _defaults = {
56         'active': lambda *a: 1,
57     }
58
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.
64     """
65     _name = "crm.case.stage"
66     _description = "Stage of case"
67     _rec_name = 'name'
68     _order = "sequence"
69
70     _columns = {
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."),
81     }
82
83     _defaults = {
84         'sequence': lambda *args: 1,
85         'probability': lambda *args: 0.0,
86         'state': 'draft',
87         'fold': False,
88     }
89
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"
95
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))
98
99     _columns = {
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'),
116     }
117     
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)
120         return ids
121
122     _defaults = {
123         'active': lambda *a: 1,
124         'allow_unlink': lambda *a: 1,
125         'stage_ids': _get_stage_common
126     }
127
128     _sql_constraints = [
129         ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !')
130     ]
131
132     _constraints = [
133         (osv.osv._check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id'])
134     ]
135
136     def name_get(self, cr, uid, ids, context=None):
137         """Overrides orm name_get method"""
138         if not isinstance(ids, list) :
139             ids = [ids]
140         res = []
141         if not ids:
142             return res
143         reads = self.read(cr, uid, ids, ['name', 'parent_id'], context)
144
145         for record in reads:
146             name = record['name']
147             if record['parent_id']:
148                 name = record['parent_id'][1] + ' / ' + name
149             res.append((record['id'], name))
150         return res
151
152 class crm_case_categ(osv.osv):
153     """ Category of Case """
154     _name = "crm.case.categ"
155     _description = "Category of Case"
156     _columns = {
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'),
160     }
161
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
167
168     _defaults = {
169         'object_id' : _find_object_id
170     }
171
172 class crm_case_resource_type(osv.osv):
173     """ Resource Type of case """
174     _name = "crm.case.resource.type"
175     _description = "Campaign"
176     _rec_name = "name"
177     _columns = {
178         'name': fields.char('Campaign Name', size=64, required=True, translate=True),
179         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
180     }
181
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)
190     """
191
192     def case_open(self, cr, uid, ids, context=None):
193         """ Opens case """
194         cases = self.browse(cr, uid, ids, context=context)
195         for case in cases:
196             values = {'active': True}
197             if case.state == 'draft':
198                 values['date_open'] = fields.datetime.now()
199             if not case.user_id:
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)
203         return True
204
205     def case_close(self, cr, uid, ids, context=None):
206         """ Closes case """
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)
209         return True
210
211     def case_cancel(self, cr, uid, ids, context=None):
212         """ Cancels case """
213         self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
214         self.case_cancel_send_note(cr, uid, ids, context=context)
215         return True
216
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)
221         return True
222
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)
227         return True
228     
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
232             case setting.
233             
234             :params: state_name: the new value of the state, such as 
235                      'draft' or 'close'.
236             :params: update_values: values that will be added with the state
237                      update when writing values to the record.
238         """
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:
242             update_values = {}
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)
246
247     def _action(self, cr, uid, cases, state_to, scrit=None, context=None):
248         if context is None:
249             context = {}
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)
256     
257     # ******************************
258     # Notifications
259     # ******************************
260     
261         def case_get_note_msg_prefix(self, cr, uid, id, context=None):
262                 return ''
263         
264     def case_open_send_note(self, cr, uid, ids, context=None):
265         for id in ids:
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)
268         return True
269
270     def case_close_send_note(self, cr, uid, ids, context=None):
271         for id in ids:
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)
274         return True
275
276     def case_cancel_send_note(self, cr, uid, ids, context=None):
277         for id in ids:
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)
280         return True
281
282     def case_pending_send_note(self, cr, uid, ids, context=None):
283         for id in ids:
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)
286         return True
287
288     def case_reset_send_note(self, cr, uid, ids, context=None):
289         for id in ids:
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)
292         return True
293
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)
305     """
306
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
310         """
311         if context is None:
312             context = {}
313         if not context.get('portal', False):
314             return 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
319
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
323         """
324         if not context.get('portal', False):
325             return False
326         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
327         return user.user_email
328
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
332         """
333         if context and context.get('portal', False):
334             return False
335         return uid
336
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
341         """
342         data = {'value': {'email_from': False, 'phone':False}}
343         if add:
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']
349         return data
350
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
355         """
356         data={}
357         if  part:
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}
361
362     def _get_default_section(self, cr, uid, context=None):
363         """ Gives default section by checking if present in the context """
364         if context is None:
365             context = {}
366         if context.get('portal', False):
367             return 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]
375         return False
376
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)
381     
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
392         """
393         domain = list(domain)
394         if section_id:
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)
397         if stage_ids:
398             return stage_ids[0]
399         return False
400
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
404         """
405         if isinstance(cases, (int, long)):
406             cases = self.browse(cr, uid, cases, context=context)
407         for case in cases:
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)
410             if stage_id:
411                 self.stage_set(cr, uid, [case.id], stage_id, context=context)
412         return True
413
414     def stage_set(self, cr, uid, ids, stage_id, context=None):
415         value = {}
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)
420
421     def stage_change(self, cr, uid, ids, op, order, context=None):
422         for case in self.browse(cr, uid, ids, context=context):
423             seq = 0
424             if case.stage_id:
425                 seq = case.stage_id.sequence
426             section_id = None
427             if case.section_id:
428                 section_id = case.section_id.id
429             next_stage_id = self.stage_find(cr, uid, section_id, [('sequence',op,seq)],order)
430             if next_stage_id:
431                 return self.stage_set(cr, uid, [case.id], next_stage_id, context=context)
432         return False
433
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
437         """
438         return self.stage_change(cr, uid, ids, '>','sequence', context)
439
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
443         """
444         return self.stage_change(cr, uid, ids, '<', 'sequence desc', context)
445
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
449             exist."""
450         if default is None:
451             default = {}
452
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)
459
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
464         for case in cases:
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
471             else:
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)
477         return True
478
479     def case_open(self, cr, uid, ids, context=None):
480         """ Opens case """
481         cases = self.browse(cr, uid, ids, context=context)
482         for case in cases:
483             data = {'active': True}
484             if case.stage_id and case.stage_id.state == 'draft':
485                  data['date_open'] = fields.datetime.now()
486             if not case.user_id:
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)
490         return True
491         
492     def case_close(self, cr, uid, ids, context=None):
493         """ Closes case """
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)
496         return True
497
498     def case_cancel(self, cr, uid, ids, context=None):
499         """ Cancels case """
500         self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
501         self.case_cancel_send_note(cr, uid, ids, context=context)
502         return True
503
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)
508         return True
509
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)
514         return True
515
516     def case_set(self, cr, uid, ids, new_state_name=None, values_to_update=None, new_stage_id=None, context=None):
517         """ TODO """
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
521         if new_state_name:
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)
525         # 2. update values
526         if values_to_update:
527             self.write(cr, uid, ids, values_to_update, context=context)
528         # 3. call _action for base action rule
529         if new_state_name:
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)
535         return True
536     
537     def remind_partner(self, cr, uid, ids, context=None, attach=False):
538         return self.remind_user(cr, uid, ids, context, attach,
539                 destination=False)
540
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:
545                 return False
546             if not case.user_id.user_email:
547                 return False
548             if destination and case.section_id.user_id:
549                 case_email = case.section_id.user_id.user_email
550             else:
551                 case_email = case.user_id.user_email
552
553             src = case_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
559                     break
560
561             if not destination:
562                 src, dest = dest, case.email_from
563                 if body and case.user_id.signature:
564                     if body:
565                         body += '\n\n%s' % (case.user_id.signature)
566                     else:
567                         body = '\n\n%s' % (case.user_id.signature)
568
569             body = self.format_body(body)
570
571             attach_to_send = {}
572
573             if attach:
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))
577
578             # Send an email
579             subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
580             mail_message.schedule_with_attach(cr, uid,
581                 src,
582                 [dest],
583                 subject,
584                 body,
585                 model=self._name,
586                 reply_to=case.section_id.reply_to,
587                 res_id=case.id,
588                 attachments=attach_to_send,
589                 context=context
590             )
591         return True
592
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
596         """
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')))
603
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)
607
608     def format_body(self, body):
609         return self.pool.get('base.action.rule').format_body(body)
610
611     def format_mail(self, obj, body):
612         return self.pool.get('base.action.rule').format_mail(obj, body)
613
614     def message_thread_followers(self, cr, uid, ids, context=None):
615         res = {}
616         for case in self.browse(cr, uid, ids, context=context):
617             l=[]
618             if case.email_cc:
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)
622             res[case.id] = l
623         return res
624     
625     # ******************************
626     # Notifications
627     # ******************************
628     
629     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
630         return ''
631     
632     def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
633         for id in ids:
634             if new_section:
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)
636             else:
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)
639         return True
640
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]
647
648 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: