[IMP] crm_stage: add a default method to send notifications when changing stages...
[odoo/odoo.git] / addons / base_status / base_stage.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 from osv import fields
23 from tools.translate import _
24
25 class base_stage(object):
26     """ Base utility mixin class for objects willing to manage their stages.
27         Object that inherit from this class should inherit from mailgate.thread
28         to have access to the mail gateway, as well as Chatter. Objects 
29         subclassing this class should define the following colums:
30         - ``date_open`` (datetime field)
31         - ``date_closed`` (datetime field)
32         - ``user_id`` (many2one to res.users)
33         - ``partner_id`` (many2one to res.partner)
34         - ``stage_id`` (many2one to a stage definition model)
35         - ``state`` (selection field, related to the stage_id.state)
36     """
37
38     def _get_default_partner(self, cr, uid, context=None):
39         """ Gives id of partner for current user
40             :param context: if portal in context is false return false anyway
41         """
42         if context is None:
43             context = {}
44         if not context or not context.get('portal'):
45             return False
46         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
47         if hasattr(user, 'partner_address_id') and user.partner_address_id:
48             return user.partner_address_id
49         return user.company_id.partner_id.id
50
51     def _get_default_email(self, cr, uid, context=None):
52         """ Gives default email address for current user
53             :param context: if portal in context is false return false anyway
54         """
55         if context is None:
56             context = {}
57         if not context or not context.get('portal'):
58             return False
59         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
60         return user.user_email
61
62     def _get_default_user(self, cr, uid, context=None):
63         """ Gives current user id
64             :param context: if portal in context is false return false anyway
65         """
66         if context is None:
67             context = {}
68         if not context or not context.get('portal'):
69             return False
70         return uid
71
72     def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
73         """ This function returns value of partner email based on Partner Address
74             :param add: Id of Partner's address
75             :param email: Partner's email ID
76         """
77         data = {'value': {'email_from': False, 'phone':False}}
78         if add:
79             address = self.pool.get('res.partner').browse(cr, uid, add)
80             data['value'] = {'email_from': address and address.email or False ,
81                              'phone':  address and address.phone or False}
82         if 'phone' not in self._columns:
83             del data['value']['phone']
84         return data
85
86     def onchange_partner_id(self, cr, uid, ids, part, email=False):
87         """ This function returns value of partner address based on partner
88             :param part: Partner's id
89             :param email: Partner's email ID
90         """
91         data={}
92         if  part:
93             addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
94             data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
95         return {'value': data}
96
97     def _get_default_section_id(self, cr, uid, context=None):
98         """ Gives default section """
99         return False
100
101     def _get_default_stage_id(self, cr, uid, context=None):
102         """ Gives default stage_id """
103         return self.stage_find(cr, uid, [], None, [('state', '=', 'draft')], context=context)
104
105     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
106         """ Find stage, with a given (optional) domain on the search,
107             ordered by the order parameter. If several stages match the 
108             search criterions, the first one will be returned, according
109             to the requested search order.
110             This method is meant to be overriden by subclasses. That way
111             specific behaviors can be achieved for every class inheriting
112             from base_stage.
113             
114             :param cases: browse_record of cases
115             :param section_id: section limitating the search, given for
116                                a generic search (for example default search).
117                                A section models concepts such as Sales team
118                                (for CRM), ou departments (for HR).
119             :param domain: a domain on the search of stages
120             :param order: order of the search
121         """
122         return False
123
124     def stage_set_with_state_name(self, cr, uid, cases, state_name, context=None):
125         """ Set a new stage, with a state_name instead of a stage_id
126             :param cases: browse_record of cases
127         """
128         if isinstance(cases, (int, long)):
129             cases = self.browse(cr, uid, cases, context=context)
130         for case in cases:
131             stage_id = self.stage_find(cr, uid, [case], None, [('state', '=', state_name)], context=context)
132             if stage_id:
133                 self.stage_set(cr, uid, [case.id], stage_id, context=context)
134         return True
135
136     def stage_set(self, cr, uid, ids, stage_id, context=None):
137         """ Set the new stage. This methods is the right method to call
138             when changing states. It also checks whether an onchange is
139             defined, and execute it.
140         """
141         value = {}
142         if hasattr(self, 'onchange_stage_id'):
143             value = self.onchange_stage_id(cr, uid, ids, stage_id, context=context)['value']
144         value['stage_id'] = stage_id
145         self.stage_set_send_note(cr, uid, ids, stage_id, context=context)
146         return self.write(cr, uid, ids, value, context=context)
147
148     def stage_change(self, cr, uid, ids, op, order, context=None):
149         """ Change the stage and take the next one, based on a condition
150             writen for the 'sequence' field and an operator. This methods
151             checks whether the case has a current stage, and takes its
152             sequence. Otherwise, a default 0 sequence is chosen and this
153             method will therefore choose the first available stage.
154             For example if op is '>' and current stage has a sequence of
155             10, this will call stage_find, with [('sequence', '>', '10')].
156         """
157         for case in self.browse(cr, uid, ids, context=context):
158             seq = 0
159             if case.stage_id:
160                 seq = case.stage_id.sequence or 0
161             section_id = None
162             next_stage_id = self.stage_find(cr, uid, [case], None, [('sequence', op, seq)],order, context=context)
163             if next_stage_id:
164                 return self.stage_set(cr, uid, [case.id], next_stage_id, context=context)
165         return False
166
167     def stage_next(self, cr, uid, ids, context=None):
168         """ This function computes next stage for case from its current stage
169             using available stage for that case type
170         """
171         return self.stage_change(cr, uid, ids, '>','sequence', context)
172
173     def stage_previous(self, cr, uid, ids, context=None):
174         """ This function computes previous stage for case from its current
175             stage using available stage for that case type
176         """
177         return self.stage_change(cr, uid, ids, '<', 'sequence desc', context)
178
179     def copy(self, cr, uid, id, default=None, context=None):
180         """ Overrides orm copy method to avoid copying messages,
181             as well as date_closed and date_open columns if they
182             exist."""
183         if default is None:
184             default = {}
185
186         if hasattr(self, '_columns'):
187             if self._columns.get('date_closed'):
188                 default.update({ 'date_closed': False, })
189             if self._columns.get('date_open'):
190                 default.update({ 'date_open': False })
191         return super(base_stage, self).copy(cr, uid, id, default, context=context)
192
193     def case_escalate(self, cr, uid, ids, context=None):
194         """ Escalates case to parent level """
195         cases = self.browse(cr, uid, ids, context=context)
196         cases[0].state # fill browse record cache, for _action having old and new values
197         for case in cases:
198             data = {'active': True}
199             if case.section_id.parent_id:
200                 data['section_id'] = case.section_id.parent_id.id
201                 if case.section_id.parent_id.change_responsible:
202                     if case.section_id.parent_id.user_id:
203                         data['user_id'] = case.section_id.parent_id.user_id.id
204             else:
205                 raise osv.except_osv(_('Error !'), _('You can not escalate, you are already at the top level regarding your sales-team category.'))
206             self.write(cr, uid, [case.id], data, context=context)
207             case.case_escalate_send_note(case.section_id.parent_id, context=context)
208         cases = self.browse(cr, uid, ids, context=context)
209         self._action(cr, uid, cases, 'escalate', context=context)
210         return True
211
212     def case_open(self, cr, uid, ids, context=None):
213         """ Opens case """
214         cases = self.browse(cr, uid, ids, context=context)
215         for case in cases:
216             data = {'active': True}
217             if case.stage_id and case.stage_id.state == 'draft':
218                 data['date_open'] = fields.datetime.now()
219             if not case.user_id:
220                 data['user_id'] = uid
221             self.case_set(cr, uid, [case.id], 'open', data, context=context)
222             self.case_open_send_note(cr, uid, [case.id], context=context)
223         return True
224         
225     def case_close(self, cr, uid, ids, context=None):
226         """ Closes case """
227         self.case_set(cr, uid, ids, 'done', {'active': True, 'date_closed': fields.datetime.now()}, context=context)
228         self.case_close_send_note(cr, uid, ids, context=context)
229         return True
230
231     def case_cancel(self, cr, uid, ids, context=None):
232         """ Cancels case """
233         self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
234         self.case_cancel_send_note(cr, uid, ids, context=context)
235         return True
236
237     def case_pending(self, cr, uid, ids, context=None):
238         """ Set case as pending """
239         self.case_set(cr, uid, ids, 'pending', {'active': True}, context=context)
240         self.case_pending_send_note(cr, uid, ids, context=context)
241         return True
242
243     def case_reset(self, cr, uid, ids, context=None):
244         """ Resets case as draft """
245         self.case_set(cr, uid, ids, 'draft', {'active': True}, context=context)
246         self.case_reset_send_note(cr, uid, ids, context=context)
247         return True
248
249     def case_set(self, cr, uid, ids, new_state_name=None, values_to_update=None, new_stage_id=None, context=None):
250         """ TODO """
251         cases = self.browse(cr, uid, ids, context=context)
252         cases[0].state # fill browse record cache, for _action having old and new values
253         # 1. update the stage
254         if new_state_name:
255             self.stage_set_with_state_name(cr, uid, cases, new_state_name, context=context)
256         elif not (new_stage_id is None):
257             self.stage_set(cr, uid, ids, new_stage_id, context=context)
258         # 2. update values
259         if values_to_update:
260             self.write(cr, uid, ids, values_to_update, context=context)
261         # 3. call _action for base action rule
262         if new_state_name:
263             self._action(cr, uid, cases, new_state_name, context=context)
264         elif not (new_stage_id is None):
265             new_state_name = self.read(cr, uid, ids, ['state'], context=context)[0]['state']
266         self._action(cr, uid, cases, new_state_name, context=context)
267         return True
268
269     def _action(self, cr, uid, cases, state_to, scrit=None, context=None):
270         if context is None:
271             context = {}
272         context['state_to'] = state_to
273         rule_obj = self.pool.get('base.action.rule')
274         if not rule_obj:
275             return True
276         model_obj = self.pool.get('ir.model')
277         model_ids = model_obj.search(cr, uid, [('model','=',self._name)], context=context)
278         rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])], context=context)
279         return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
280
281     def remind_partner(self, cr, uid, ids, context=None, attach=False):
282         return self.remind_user(cr, uid, ids, context, attach,
283                 destination=False)
284
285     def remind_user(self, cr, uid, ids, context=None, attach=False, destination=True):
286         mail_message = self.pool.get('mail.message')
287         for case in self.browse(cr, uid, ids, context=context):
288             if not destination and not case.email_from:
289                 return False
290             if not case.user_id.user_email:
291                 return False
292             if destination and case.section_id.user_id:
293                 case_email = case.section_id.user_id.user_email
294             else:
295                 case_email = case.user_id.user_email
296
297             src = case_email
298             dest = case.user_id.user_email or ""
299             body = case.description or ""
300             for message in case.message_ids:
301                 if message.email_from and message.body_text:
302                     body = message.body_text
303                     break
304
305             if not destination:
306                 src, dest = dest, case.email_from
307                 if body and case.user_id.signature:
308                     if body:
309                         body += '\n\n%s' % (case.user_id.signature)
310                     else:
311                         body = '\n\n%s' % (case.user_id.signature)
312
313             body = self.format_body(body)
314
315             attach_to_send = {}
316
317             if attach:
318                 attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
319                 attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname', 'datas'])
320                 attach_to_send = dict(map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send))
321
322             # Send an email
323             subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
324             mail_message.schedule_with_attach(cr, uid,
325                 src,
326                 [dest],
327                 subject,
328                 body,
329                 model=self._name,
330                 reply_to=case.section_id.reply_to,
331                 res_id=case.id,
332                 attachments=attach_to_send,
333                 context=context
334             )
335         return True
336
337     def _check(self, cr, uid, ids=False, context=None):
338         """ Function called by the scheduler to process cases for date actions.
339             Must be overriden by inheriting classes.
340         """
341         return True
342
343     def format_body(self, body):
344         return self.pool.get('base.action.rule').format_body(body)
345
346     def format_mail(self, obj, body):
347         return self.pool.get('base.action.rule').format_mail(obj, body)
348
349     def message_thread_followers(self, cr, uid, ids, context=None):
350         res = {}
351         for case in self.browse(cr, uid, ids, context=context):
352             l=[]
353             if case.email_cc:
354                 l.append(case.email_cc)
355             if case.user_id and case.user_id.user_email:
356                 l.append(case.user_id.user_email)
357             res[case.id] = l
358         return res
359     
360     # ******************************
361     # Notifications
362     # ******************************
363     
364         def case_get_note_msg_prefix(self, cr, uid, id, context=None):
365                 return ''
366         
367     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
368         """ Send a notification when the stage changes. This method has
369             to be overriden, because each document will have its particular
370             behavior and/or stage model (such as project.task.type or
371             crm.case.stage).
372         """
373         return True
374     
375     def case_open_send_note(self, cr, uid, ids, context=None):
376         for id in ids:
377             msg = _('%s has been <b>opened</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
378             self.message_append_note(cr, uid, [id], body=msg, context=context)
379         return True
380
381     def case_close_send_note(self, cr, uid, ids, context=None):
382         for id in ids:
383             msg = _('%s has been <b>closed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
384             self.message_append_note(cr, uid, [id], body=msg, context=context)
385         return True
386
387     def case_cancel_send_note(self, cr, uid, ids, context=None):
388         for id in ids:
389             msg = _('%s has been <b>canceled</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
390             self.message_append_note(cr, uid, [id], body=msg, context=context)
391         return True
392
393     def case_pending_send_note(self, cr, uid, ids, context=None):
394         for id in ids:
395             msg = _('%s is now <b>pending</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
396             self.message_append_note(cr, uid, [id], body=msg, context=context)
397         return True
398
399     def case_reset_send_note(self, cr, uid, ids, context=None):
400         for id in ids:
401             msg = _('%s has been <b>renewed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
402             self.message_append_note(cr, uid, [id], body=msg, context=context)
403         return True
404     
405     def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
406         for id in ids:
407             if new_section:
408                 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)
409             else:
410                 msg = '%s has been <b>escalated</b>.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
411             self.message_append_note(cr, uid, [id], 'System Notification', msg, context=context)
412         return True