[IMP]update __openerp__.py in all modules
[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, osv
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 not in context returns False
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 not in context returns False
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.email
61
62     def _get_default_user(self, cr, uid, context=None):
63         """ Gives current user id
64             :param context: if portal not in context returns False
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 are already at the top level of your sales-team category.\nTherefore you cannot escalate furthermore."))
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         """ Generic method for setting case. This methods wraps the update
251             of the record, as well as call to _action and browse_record
252             case setting to fill the cache.
253
254             :params new_state_name: the new state of the record; this method
255                                     will call ``stage_set_with_state_name``
256                                     that will find the stage matching the
257                                     new state, using the ``stage_find`` method.
258             :params new_stage_id: alternatively, you may directly give the
259                                   new stage of the record
260             :params state_name: the new value of the state, such as
261                      'draft' or 'close'.
262             :params update_values: values that will be added with the state
263                      update when writing values to the record.
264         """
265         cases = self.browse(cr, uid, ids, context=context)
266         cases[0].state # fill browse record cache, for _action having old and new values
267         # 1. update the stage
268         if new_state_name:
269             self.stage_set_with_state_name(cr, uid, cases, new_state_name, context=context)
270         elif not (new_stage_id is None):
271             self.stage_set(cr, uid, ids, new_stage_id, context=context)
272         # 2. update values
273         if values_to_update:
274             self.write(cr, uid, ids, values_to_update, context=context)
275         # 3. call _action for base action rule
276         if new_state_name:
277             self._action(cr, uid, cases, new_state_name, context=context)
278         elif not (new_stage_id is None):
279             new_state_name = self.read(cr, uid, ids, ['state'], context=context)[0]['state']
280         self._action(cr, uid, cases, new_state_name, context=context)
281         return True
282
283     def _action(self, cr, uid, cases, state_to, scrit=None, context=None):
284         if context is None:
285             context = {}
286         context['state_to'] = state_to
287         rule_obj = self.pool.get('base.action.rule')
288         if not rule_obj:
289             return True
290         model_obj = self.pool.get('ir.model')
291         model_ids = model_obj.search(cr, uid, [('model','=',self._name)], context=context)
292         rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])], context=context)
293         return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
294
295     def remind_partner(self, cr, uid, ids, context=None, attach=False):
296         return self.remind_user(cr, uid, ids, context, attach,
297                 destination=False)
298
299     def remind_user(self, cr, uid, ids, context=None, attach=False, destination=True):
300         mail_message = self.pool.get('mail.message')
301         for case in self.browse(cr, uid, ids, context=context):
302             if not destination and not case.email_from:
303                 return False
304             if not case.user_id.email:
305                 return False
306             if destination and case.section_id.user_id:
307                 case_email = case.section_id.user_id.email
308             else:
309                 case_email = case.user_id.email
310
311             src = case_email
312             dest = case.user_id.email or ""
313             body = case.description or ""
314             for message in case.message_ids:
315                 if message.email_from and message.body_text:
316                     body = message.body_text
317                     break
318
319             if not destination:
320                 src, dest = dest, case.email_from
321                 if body and case.user_id.signature:
322                     if body:
323                         body += '\n\n%s' % (case.user_id.signature)
324                     else:
325                         body = '\n\n%s' % (case.user_id.signature)
326
327             body = self.format_body(body)
328
329             attach_to_send = {}
330
331             if attach:
332                 attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
333                 attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname', 'datas'])
334                 attach_to_send = dict(map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send))
335
336             # Send an email
337             subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
338             mail_message.schedule_with_attach(cr, uid,
339                 src,
340                 [dest],
341                 subject,
342                 body,
343                 model=self._name,
344                 reply_to=case.section_id.reply_to,
345                 res_id=case.id,
346                 attachments=attach_to_send,
347                 context=context
348             )
349         return True
350
351     def _check(self, cr, uid, ids=False, context=None):
352         """ Function called by the scheduler to process cases for date actions.
353             Must be overriden by inheriting classes.
354         """
355         return True
356
357     def format_body(self, body):
358         return self.pool.get('base.action.rule').format_body(body)
359
360     def format_mail(self, obj, body):
361         return self.pool.get('base.action.rule').format_mail(obj, body)
362
363     def message_thread_followers(self, cr, uid, ids, context=None):
364         res = {}
365         for case in self.browse(cr, uid, ids, context=context):
366             l=[]
367             if case.email_cc:
368                 l.append(case.email_cc)
369             if case.user_id and case.user_id.email:
370                 l.append(case.user_id.email)
371             res[case.id] = l
372         return res
373
374     # ******************************
375     # Notifications
376     # ******************************
377
378     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
379         """ Default prefix for notifications. For example: "%s has been
380             <b>closed</b>.". As several models will inherit from base_stage,
381             this method returns a void string. Class using base_stage
382             will have to override this method to define the prefix they
383             want to display.
384         """
385         return ''
386
387     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
388         """ Send a notification when the stage changes. This method has
389             to be overriden, because each document will have its particular
390             behavior and/or stage model (such as project.task.type or
391             crm.case.stage).
392         """
393         return True
394
395     def case_open_send_note(self, cr, uid, ids, context=None):
396         for id in ids:
397             msg = _('%s has been <b>opened</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
398             self.message_append_note(cr, uid, [id], body=msg, context=context)
399         return True
400
401     def case_close_send_note(self, cr, uid, ids, context=None):
402         for id in ids:
403             msg = _('%s has been <b>closed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
404             self.message_append_note(cr, uid, [id], body=msg, context=context)
405         return True
406
407     def case_cancel_send_note(self, cr, uid, ids, context=None):
408         for id in ids:
409             msg = _('%s has been <b>canceled</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
410             self.message_append_note(cr, uid, [id], body=msg, context=context)
411         return True
412
413     def case_pending_send_note(self, cr, uid, ids, context=None):
414         for id in ids:
415             msg = _('%s is now <b>pending</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
416             self.message_append_note(cr, uid, [id], body=msg, context=context)
417         return True
418
419     def case_reset_send_note(self, cr, uid, ids, context=None):
420         for id in ids:
421             msg = _('%s has been <b>renewed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
422             self.message_append_note(cr, uid, [id], body=msg, context=context)
423         return True
424
425     def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
426         for id in ids:
427             if new_section:
428                 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)
429             else:
430                 msg = '%s has been <b>escalated</b>.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
431             self.message_append_note(cr, uid, [id], 'System Notification', msg, context=context)
432         return True