[IMP] project.task: second step towards cleaning.
[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',
80                         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."),
81         'fold': fields.boolean('Hide in Views when Empty',
82                         help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
83         'type': fields.selection([  ('lead','Lead'),
84                                     ('opportunity', 'Opportunity'),
85                                     ('both', 'Both')],
86                                     string='Type', size=16, required=True,
87                                     help="This field is used to distinguish stages related to Leads from stages related to Opportunities, or to specify stages available for both types."),
88     }
89
90     _defaults = {
91         'sequence': lambda *args: 1,
92         'probability': lambda *args: 0.0,
93         'state': 'draft',
94         'fold': False,
95         'type': 'both',
96     }
97
98 class crm_case_section(osv.osv):
99     """ Model for sales teams. """
100     _name = "crm.case.section"
101     _description = "Sales Teams"
102     _order = "complete_name"
103
104     def get_full_name(self, cr, uid, ids, field_name, arg, context=None):
105         return  dict(self.name_get(cr, uid, ids, context=context))
106
107     _columns = {
108         'name': fields.char('Sales Team', size=64, required=True, translate=True),
109         'complete_name': fields.function(get_full_name, type='char', size=256, readonly=True, store=True),
110         'code': fields.char('Code', size=8),
111         'active': fields.boolean('Active', help="If the active field is set to "\
112                         "true, it will allow you to hide the sales team without removing it."),
113         'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"),
114         'change_responsible': fields.boolean('Reassign Escalated', help="When escalating to this team override the saleman with the team leader."),
115         'user_id': fields.many2one('res.users', 'Team Leader'),
116         'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'),
117         '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"),
118         'parent_id': fields.many2one('crm.case.section', 'Parent Team'),
119         'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'),
120         'resource_calendar_id': fields.many2one('resource.calendar', "Working Time", help="Used to compute open days"),
121         'note': fields.text('Description'),
122         'working_hours': fields.float('Working Hours', digits=(16,2 )),
123         'stage_ids': fields.many2many('crm.case.stage', 'section_stage_rel', 'section_id', 'stage_id', 'Stages'),
124     }
125     
126     def _get_stage_common(self, cr, uid, context):
127         ids = self.pool.get('crm.case.stage').search(cr, uid, [('case_default','=',1)], context=context)
128         return ids
129
130     _defaults = {
131         'active': lambda *a: 1,
132         'allow_unlink': lambda *a: 1,
133         'stage_ids': _get_stage_common
134     }
135
136     _sql_constraints = [
137         ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !')
138     ]
139
140     _constraints = [
141         (osv.osv._check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id'])
142     ]
143
144     def name_get(self, cr, uid, ids, context=None):
145         """Overrides orm name_get method"""
146         if not isinstance(ids, list) :
147             ids = [ids]
148         res = []
149         if not ids:
150             return res
151         reads = self.read(cr, uid, ids, ['name', 'parent_id'], context)
152
153         for record in reads:
154             name = record['name']
155             if record['parent_id']:
156                 name = record['parent_id'][1] + ' / ' + name
157             res.append((record['id'], name))
158         return res
159
160 class crm_case_categ(osv.osv):
161     """ Category of Case """
162     _name = "crm.case.categ"
163     _description = "Category of Case"
164     _columns = {
165         'name': fields.char('Name', size=64, required=True, translate=True),
166         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
167         'object_id': fields.many2one('ir.model', 'Object Name'),
168     }
169
170     def _find_object_id(self, cr, uid, context=None):
171         """Finds id for case object"""
172         object_id = context and context.get('object_id', False) or False
173         ids = self.pool.get('ir.model').search(cr, uid, [('id', '=', object_id)])
174         return ids and ids[0] or False
175
176     _defaults = {
177         'object_id' : _find_object_id
178     }
179
180 class crm_case_resource_type(osv.osv):
181     """ Resource Type of case """
182     _name = "crm.case.resource.type"
183     _description = "Campaign"
184     _rec_name = "name"
185     _columns = {
186         'name': fields.char('Campaign Name', size=64, required=True, translate=True),
187         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
188     }
189
190 class crm_base(object):
191     """ Base utility mixin class for objects willing to manage their state.
192         Object subclassing this class should define the following colums:
193         - ``date_open`` (datetime field)
194         - ``date_closed`` (datetime field)
195         - ``user_id`` (many2one to res.users)
196         - ``partner_id`` (many2one to res.partner)
197         - ``state`` (selection field)
198     """
199
200     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
201         """ This function returns value of partner email based on Partner Address
202             :param add: Id of Partner's address
203             :param email: Partner's email ID
204         """
205         data = {'value': {'email_from': False, 'phone':False}}
206         if add:
207             address = self.pool.get('res.partner').browse(cr, uid, add)
208             data['value'] = {'email_from': address and address.email or False ,
209                              'phone':  address and address.phone or False}
210         if 'phone' not in self._columns:
211             del data['value']['phone']
212         return data
213
214     def onchange_partner_id(self, cr, uid, ids, part, email=False):
215         """ This function returns value of partner address based on partner
216             :param part: Partner's id
217             :param email: Partner's email ID
218         """
219         data={}
220         if  part:
221             addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
222             data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
223         return {'value': data}
224
225     def case_open(self, cr, uid, ids, context=None):
226         """ Opens case """
227         cases = self.browse(cr, uid, ids, context=context)
228         for case in cases:
229             values = {'active': True}
230             if case.state == 'draft':
231                 values['date_open'] = fields.datetime.now()
232             if not case.user_id:
233                 values['user_id'] = uid
234             self.case_set(cr, uid, [case.id], 'open', values, context=context)
235             self.case_open_send_note(cr, uid, [case.id], context=context)
236         return True
237
238     def case_close(self, cr, uid, ids, context=None):
239         """ Closes case """
240         self.case_set(cr, uid, ids, 'done', {'date_closed': fields.datetime.now()}, context=context)
241         self.case_close_send_note(cr, uid, ids, context=context)
242         return True
243
244     def case_cancel(self, cr, uid, ids, context=None):
245         """ Cancels case """
246         self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
247         self.case_cancel_send_note(cr, uid, ids, context=context)
248         return True
249
250     def case_pending(self, cr, uid, ids, context=None):
251         """ Sets case as pending """
252         self.case_set(cr, uid, ids, 'pending', {'active': True}, context=context)
253         self.case_pending_send_note(cr, uid, ids, context=context)
254         return True
255
256     def case_reset(self, cr, uid, ids, context=None):
257         """ Resets case as draft """
258         self.case_set(cr, uid, ids, 'draft', {'active': True}, context=context)
259         self.case_reset_send_note(cr, uid, ids, context=context)
260         return True
261     
262     def case_set(self, cr, uid, ids, state_name, update_values=None, context=None):
263         """ Generic method for setting case. This methods wraps the update
264             of the record, as well as call to _action and browse record
265             case setting.
266             
267             :params: state_name: the new value of the state, such as 
268                      'draft' or 'close'.
269             :params: update_values: values that will be added with the state
270                      update when writing values to the record.
271         """
272         cases = self.browse(cr, uid, ids, context=context)
273         cases[0].state # fill browse record cache, for _action having old and new values
274         if update_values is None:
275             update_values = {}
276         update_values.update({'state': state_name})
277         self.write(cr, uid, ids, update_values, context=context)
278         self._action(cr, uid, cases, state_name, context=context)
279
280     def _action(self, cr, uid, cases, state_to, scrit=None, context=None):
281         if context is None:
282             context = {}
283         context['state_to'] = state_to
284         rule_obj = self.pool.get('base.action.rule')
285         model_obj = self.pool.get('ir.model')
286         model_ids = model_obj.search(cr, uid, [('model','=',self._name)])
287         rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])])
288         return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
289     
290     # ******************************
291     # Notifications
292     # ******************************
293     
294         def case_get_note_msg_prefix(self, cr, uid, id, context=None):
295                 return ''
296         
297     def case_open_send_note(self, cr, uid, ids, context=None):
298         for id in ids:
299             msg = _('%s has been <b>opened</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
300             self.message_append_note(cr, uid, [id], body=msg, context=context)
301         return True
302
303     def case_close_send_note(self, cr, uid, ids, context=None):
304         for id in ids:
305             msg = _('%s has been <b>closed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
306             self.message_append_note(cr, uid, [id], body=msg, context=context)
307         return True
308
309     def case_cancel_send_note(self, cr, uid, ids, context=None):
310         for id in ids:
311             msg = _('%s has been <b>canceled</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
312             self.message_append_note(cr, uid, [id], body=msg, context=context)
313         return True
314
315     def case_pending_send_note(self, cr, uid, ids, context=None):
316         for id in ids:
317             msg = _('%s is now <b>pending</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
318             self.message_append_note(cr, uid, [id], body=msg, context=context)
319         return True
320
321     def case_reset_send_note(self, cr, uid, ids, context=None):
322         for id in ids:
323             msg = _('%s has been <b>renewed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
324             self.message_append_note(cr, uid, [id], body=msg, context=context)
325         return True
326
327 class crm_case(object):
328     """ Base utility mixin class for objects willing to manage their stages.
329         Object that inherit from this class should inherit from mailgate.thread
330         to have access to the mail gateway, as well as Chatter. Objects 
331         subclassing this class should define the following colums:
332         - ``date_open`` (datetime field)
333         - ``date_closed`` (datetime field)
334         - ``user_id`` (many2one to res.users)
335         - ``partner_id`` (many2one to res.partner)
336         - ``stage_id`` (many2one to a stage definition model)
337         - ``state`` (selection field, related to the stage_id.state)
338     """
339
340     def _get_default_partner(self, cr, uid, context=None):
341         """ Gives id of partner for current user
342             :param context: if portal in context is false return false anyway
343         """
344         if context is None:
345             context = {}
346         if not context.get('portal', False):
347             return False
348         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
349         if hasattr(user, 'partner_address_id') and user.partner_address_id:
350             return user.partner_address_id
351         return user.company_id.partner_id.id
352
353     def _get_default_email(self, cr, uid, context=None):
354         """ Gives default email address for current user
355             :param context: if portal in context is false return false anyway
356         """
357         if context and context.get('portal'):
358             return False
359         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
360         return user.user_email
361
362     def _get_default_user(self, cr, uid, context=None):
363         """ Gives current user id
364             :param context: if portal in context is false return false anyway
365         """
366         if context and context.get('portal'):
367             return False
368         return uid
369
370     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
371         """ This function returns value of partner email based on Partner Address
372             :param add: Id of Partner's address
373             :param email: Partner's email ID
374         """
375         data = {'value': {'email_from': False, 'phone':False}}
376         if add:
377             address = self.pool.get('res.partner').browse(cr, uid, add)
378             data['value'] = {'email_from': address and address.email or False ,
379                              'phone':  address and address.phone or False}
380         if 'phone' not in self._columns:
381             del data['value']['phone']
382         return data
383
384     def onchange_partner_id(self, cr, uid, ids, part, email=False):
385         """ This function returns value of partner address based on partner
386             :param part: Partner's id
387             :param email: Partner's email ID
388         """
389         data={}
390         if  part:
391             addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
392             data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
393         return {'value': data}
394
395     def _get_default_section_id(self, cr, uid, context=None):
396         """ Gives default section """
397         return False
398
399     def _get_default_stage_id(self, cr, uid, context=None):
400         """ Gives default stage_id """
401         return self.stage_find(cr, uid, [], None, [('state', '=', 'draft')], context=context)
402
403     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
404         """ Find stage, with a given (optional) domain on the search,
405             ordered by the order parameter. If several stages match the 
406             search criterions, the first one will be returned, according
407             to the requested search order.
408             This method is meant to be overriden by subclasses. That way
409             specific behaviors can be achieved for every class inheriting
410             from crm_case.
411             
412             :param cases: browse_record of cases
413             :param section_id: section limitating the search, given for
414                                a generic search (for example default search).
415                                A section models concepts such as Sales team
416                                (for CRM), ou departments (for HR).
417             :param domain: a domain on the search of stages
418             :param order: order of the search
419         """
420         return False
421
422     def stage_set_with_state_name(self, cr, uid, cases, state_name, context=None):
423         """ Set a new stage, with a state_name instead of a stage_id
424             :param cases: browse_record of cases
425         """
426         if isinstance(cases, (int, long)):
427             cases = self.browse(cr, uid, cases, context=context)
428         for case in cases:
429             stage_id = self.stage_find(cr, uid, [case], None, [('state', '=', state_name)], context=context)
430             if stage_id:
431                 self.stage_set(cr, uid, [case.id], stage_id, context=context)
432         return True
433
434     def stage_set(self, cr, uid, ids, stage_id, context=None):
435         """ Set the new stage. This methods is the right method to call
436             when changing states. It also checks whether an onchange is
437             defined, and execute it.
438         """
439         value = {}
440         if hasattr(self, 'onchange_stage_id'):
441             value = self.onchange_stage_id(cr, uid, ids, stage_id, context=context)['value']
442         value['stage_id'] = stage_id
443         return self.write(cr, uid, ids, value, context=context)
444
445     def stage_change(self, cr, uid, ids, op, order, context=None):
446         """ Change the stage and take the next one, based on a condition
447             writen for the 'sequence' field and an operator. This methods
448             checks whether the case has a current stage, and takes its
449             sequence. Otherwise, a default 0 sequence is chosen and this
450             method will therefore choose the first available stage.
451             For example if op is '>' and current stage has a sequence of
452             10, this will call stage_find, with [('sequence', '>', '10')].
453         """
454         for case in self.browse(cr, uid, ids, context=context):
455             seq = 0
456             if case.stage_id:
457                 seq = case.stage_id.sequence or 0
458             section_id = None
459             next_stage_id = self.stage_find(cr, uid, [case], None, [('sequence', op, seq)],order, context=context)
460             if next_stage_id:
461                 return self.stage_set(cr, uid, [case.id], next_stage_id, context=context)
462         return False
463
464     def stage_next(self, cr, uid, ids, context=None):
465         """ This function computes next stage for case from its current stage
466             using available stage for that case type
467         """
468         return self.stage_change(cr, uid, ids, '>','sequence', context)
469
470     def stage_previous(self, cr, uid, ids, context=None):
471         """ This function computes previous stage for case from its current
472             stage using available stage for that case type
473         """
474         return self.stage_change(cr, uid, ids, '<', 'sequence desc', context)
475
476     def copy(self, cr, uid, id, default=None, context=None):
477         """ Overrides orm copy method to avoid copying messages,
478             as well as date_closed and date_open columns if they
479             exist."""
480         if default is None:
481             default = {}
482
483         if hasattr(self, '_columns'):
484             if self._columns.get('date_closed'):
485                 default.update({ 'date_closed': False, })
486             if self._columns.get('date_open'):
487                 default.update({ 'date_open': False })
488         return super(crm_case, self).copy(cr, uid, id, default, context=context)
489
490     def case_escalate(self, cr, uid, ids, context=None):
491         """ Escalates case to parent level """
492         cases = self.browse(cr, uid, ids, context=context)
493         cases[0].state # fill browse record cache, for _action having old and new values
494         for case in cases:
495             data = {'active': True}
496             if case.section_id.parent_id:
497                 data['section_id'] = case.section_id.parent_id.id
498                 if case.section_id.parent_id.change_responsible:
499                     if case.section_id.parent_id.user_id:
500                         data['user_id'] = case.section_id.parent_id.user_id.id
501             else:
502                 raise osv.except_osv(_('Error !'), _('You can not escalate, you are already at the top level regarding your sales-team category.'))
503             self.write(cr, uid, [case.id], data, context=context)
504             case.case_escalate_send_note(case.section_id.parent_id, context=context)
505         cases = self.browse(cr, uid, ids, context=context)
506         self._action(cr, uid, cases, 'escalate', context=context)
507         return True
508
509     def case_open(self, cr, uid, ids, context=None):
510         """ Opens case """
511         cases = self.browse(cr, uid, ids, context=context)
512         for case in cases:
513             data = {'active': True}
514             if case.stage_id and case.stage_id.state == 'draft':
515                 data['date_open'] = fields.datetime.now()
516             if not case.user_id:
517                 data['user_id'] = uid
518             self.case_set(cr, uid, [case.id], 'open', data, context=context)
519             self.case_open_send_note(cr, uid, [case.id], context=context)
520         return True
521         
522     def case_close(self, cr, uid, ids, context=None):
523         """ Closes case """
524         self.case_set(cr, uid, ids, 'done', {'active': True, 'date_closed': fields.datetime.now()}, context=context)
525         self.case_close_send_note(cr, uid, ids, context=context)
526         return True
527
528     def case_cancel(self, cr, uid, ids, context=None):
529         """ Cancels case """
530         self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
531         self.case_cancel_send_note(cr, uid, ids, context=context)
532         return True
533
534     def case_pending(self, cr, uid, ids, context=None):
535         """ Set case as pending """
536         self.case_set(cr, uid, ids, 'pending', {'active': True}, context=context)
537         self.case_pending_send_note(cr, uid, ids, context=context)
538         return True
539
540     def case_reset(self, cr, uid, ids, context=None):
541         """ Resets case as draft """
542         self.case_set(cr, uid, ids, 'draft', {'active': True}, context=context)
543         self.case_reset_send_note(cr, uid, ids, context=context)
544         return True
545
546     def case_set(self, cr, uid, ids, new_state_name=None, values_to_update=None, new_stage_id=None, context=None):
547         """ TODO """
548         cases = self.browse(cr, uid, ids, context=context)
549         cases[0].state # fill browse record cache, for _action having old and new values
550         # 1. update the stage
551         if new_state_name:
552             self.stage_set_with_state_name(cr, uid, cases, new_state_name, context=context)
553         elif not (new_stage_id is None):
554             self.stage_set(cr, uid, ids, new_stage_id, context=context)
555         # 2. update values
556         if values_to_update:
557             self.write(cr, uid, ids, values_to_update, context=context)
558         # 3. call _action for base action rule
559         if new_state_name:
560             self._action(cr, uid, cases, new_state_name, context=context)
561         elif not (new_stage_id is None):
562             new_state_name = self.read(cr, uid, ids, ['state'], context=context)[0]['state']
563         self._action(cr, uid, cases, new_state_name, context=context)
564         return True
565
566     def _action(self, cr, uid, cases, state_to, scrit=None, context=None):
567         if context is None:
568             context = {}
569         context['state_to'] = state_to
570         rule_obj = self.pool.get('base.action.rule')
571         if not rule_obj:
572             return True
573         model_obj = self.pool.get('ir.model')
574         model_ids = model_obj.search(cr, uid, [('model','=',self._name)], context=context)
575         rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])], context=context)
576         return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
577
578     def remind_partner(self, cr, uid, ids, context=None, attach=False):
579         return self.remind_user(cr, uid, ids, context, attach,
580                 destination=False)
581
582     def remind_user(self, cr, uid, ids, context=None, attach=False, destination=True):
583         mail_message = self.pool.get('mail.message')
584         for case in self.browse(cr, uid, ids, context=context):
585             if not destination and not case.email_from:
586                 return False
587             if not case.user_id.user_email:
588                 return False
589             if destination and case.section_id.user_id:
590                 case_email = case.section_id.user_id.user_email
591             else:
592                 case_email = case.user_id.user_email
593
594             src = case_email
595             dest = case.user_id.user_email or ""
596             body = case.description or ""
597             for message in case.message_ids:
598                 if message.email_from and message.body_text:
599                     body = message.body_text
600                     break
601
602             if not destination:
603                 src, dest = dest, case.email_from
604                 if body and case.user_id.signature:
605                     if body:
606                         body += '\n\n%s' % (case.user_id.signature)
607                     else:
608                         body = '\n\n%s' % (case.user_id.signature)
609
610             body = self.format_body(body)
611
612             attach_to_send = {}
613
614             if attach:
615                 attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
616                 attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname', 'datas'])
617                 attach_to_send = dict(map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send))
618
619             # Send an email
620             subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
621             mail_message.schedule_with_attach(cr, uid,
622                 src,
623                 [dest],
624                 subject,
625                 body,
626                 model=self._name,
627                 reply_to=case.section_id.reply_to,
628                 res_id=case.id,
629                 attachments=attach_to_send,
630                 context=context
631             )
632         return True
633
634     def _check(self, cr, uid, ids=False, context=None):
635         """Function called by the scheduler to process cases for date actions
636            Only works on not done and cancelled cases
637         """
638         cr.execute('select * from crm_case \
639                 where (date_action_last<%s or date_action_last is null) \
640                 and (date_action_next<=%s or date_action_next is null) \
641                 and state not in (\'cancel\',\'done\')',
642                 (time.strftime("%Y-%m-%d %H:%M:%S"),
643                     time.strftime('%Y-%m-%d %H:%M:%S')))
644
645         ids2 = map(lambda x: x[0], cr.fetchall() or [])
646         cases = self.browse(cr, uid, ids2, context=context)
647         return self._action(cr, uid, cases, False, context=context)
648
649     def format_body(self, body):
650         return self.pool.get('base.action.rule').format_body(body)
651
652     def format_mail(self, obj, body):
653         return self.pool.get('base.action.rule').format_mail(obj, body)
654
655     def message_thread_followers(self, cr, uid, ids, context=None):
656         res = {}
657         for case in self.browse(cr, uid, ids, context=context):
658             l=[]
659             if case.email_cc:
660                 l.append(case.email_cc)
661             if case.user_id and case.user_id.user_email:
662                 l.append(case.user_id.user_email)
663             res[case.id] = l
664         return res
665     
666     # ******************************
667     # Notifications
668     # ******************************
669     
670         def case_get_note_msg_prefix(self, cr, uid, id, context=None):
671                 return ''
672         
673     def case_open_send_note(self, cr, uid, ids, context=None):
674         for id in ids:
675             msg = _('%s has been <b>opened</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
676             self.message_append_note(cr, uid, [id], body=msg, context=context)
677         return True
678
679     def case_close_send_note(self, cr, uid, ids, context=None):
680         for id in ids:
681             msg = _('%s has been <b>closed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
682             self.message_append_note(cr, uid, [id], body=msg, context=context)
683         return True
684
685     def case_cancel_send_note(self, cr, uid, ids, context=None):
686         for id in ids:
687             msg = _('%s has been <b>canceled</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
688             self.message_append_note(cr, uid, [id], body=msg, context=context)
689         return True
690
691     def case_pending_send_note(self, cr, uid, ids, context=None):
692         for id in ids:
693             msg = _('%s is now <b>pending</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
694             self.message_append_note(cr, uid, [id], body=msg, context=context)
695         return True
696
697     def case_reset_send_note(self, cr, uid, ids, context=None):
698         for id in ids:
699             msg = _('%s has been <b>renewed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
700             self.message_append_note(cr, uid, [id], body=msg, context=context)
701         return True
702     
703     def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
704         for id in ids:
705             if new_section:
706                 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)
707             else:
708                 msg = '%s has been <b>escalated</b>.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
709             self.message_append_note(cr, uid, [id], 'System Notification', msg, context=context)
710         return True
711
712 def _links_get(self, cr, uid, context=None):
713     """Gets links value for reference field"""
714     obj = self.pool.get('res.request.link')
715     ids = obj.search(cr, uid, [])
716     res = obj.read(cr, uid, ids, ['object', 'name'], context)
717     return [(r['object'], r['name']) for r in res]
718
719 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: