[MERGE] merge with trunk
[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, context=None):
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'] = {'partner_name': address and address.name or False,
81                              'email_from': address and address.email or False,
82                              'phone':  address and address.phone or False,
83                              'street': address and address.street or False,
84                              'street2': address and address.street2 or False,
85                              'city': address and address.city or False,
86                              'state_id': address.state_id and address.state_id.id or False,
87                              'zip': address and address.zip or False,
88                              'country_id': address.country_id and address.country_id.id or False,
89                              }
90         fields = self.fields_get(cr, uid, context=context or {})
91         for key in data['value'].keys():
92             if key not in fields:
93                 del data['value'][key]
94         return data
95
96     def onchange_partner_id(self, cr, uid, ids, part, email=False):
97         """ This function returns value of partner address based on partner
98             :param part: Partner's id
99             :param email: Partner's email ID
100         """
101         data={}
102         if  part:
103             addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
104             data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value'])
105         return {'value': data}
106
107     def _get_default_section_id(self, cr, uid, context=None):
108         """ Gives default section """
109         return False
110
111     def _get_default_stage_id(self, cr, uid, context=None):
112         """ Gives default stage_id """
113         return self.stage_find(cr, uid, [], None, [('state', '=', 'draft')], context=context)
114
115     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
116         """ Find stage, with a given (optional) domain on the search,
117             ordered by the order parameter. If several stages match the
118             search criterions, the first one will be returned, according
119             to the requested search order.
120             This method is meant to be overriden by subclasses. That way
121             specific behaviors can be achieved for every class inheriting
122             from base_stage.
123
124             :param cases: browse_record of cases
125             :param section_id: section limitating the search, given for
126                                a generic search (for example default search).
127                                A section models concepts such as Sales team
128                                (for CRM), ou departments (for HR).
129             :param domain: a domain on the search of stages
130             :param order: order of the search
131         """
132         return False
133
134     def stage_set_with_state_name(self, cr, uid, cases, state_name, context=None):
135         """ Set a new stage, with a state_name instead of a stage_id
136             :param cases: browse_record of cases
137         """
138         if isinstance(cases, (int, long)):
139             cases = self.browse(cr, uid, cases, context=context)
140         for case in cases:
141             stage_id = self.stage_find(cr, uid, [case], None, [('state', '=', state_name)], context=context)
142             if stage_id:
143                 self.stage_set(cr, uid, [case.id], stage_id, context=context)
144         return True
145
146     def stage_set(self, cr, uid, ids, stage_id, context=None):
147         """ Set the new stage. This methods is the right method to call
148             when changing states. It also checks whether an onchange is
149             defined, and execute it.
150         """
151         value = {}
152         if hasattr(self, 'onchange_stage_id'):
153             value = self.onchange_stage_id(cr, uid, ids, stage_id, context=context)['value']
154         value['stage_id'] = stage_id
155         self.stage_set_send_note(cr, uid, ids, stage_id, context=context)
156         return self.write(cr, uid, ids, value, context=context)
157
158     def stage_change(self, cr, uid, ids, op, order, context=None):
159         """ Change the stage and take the next one, based on a condition
160             writen for the 'sequence' field and an operator. This methods
161             checks whether the case has a current stage, and takes its
162             sequence. Otherwise, a default 0 sequence is chosen and this
163             method will therefore choose the first available stage.
164             For example if op is '>' and current stage has a sequence of
165             10, this will call stage_find, with [('sequence', '>', '10')].
166         """
167         for case in self.browse(cr, uid, ids, context=context):
168             seq = 0
169             if case.stage_id:
170                 seq = case.stage_id.sequence or 0
171             section_id = None
172             next_stage_id = self.stage_find(cr, uid, [case], None, [('sequence', op, seq)],order, context=context)
173             if next_stage_id:
174                 return self.stage_set(cr, uid, [case.id], next_stage_id, context=context)
175         return False
176
177     def stage_next(self, cr, uid, ids, context=None):
178         """ This function computes next stage for case from its current stage
179             using available stage for that case type
180         """
181         return self.stage_change(cr, uid, ids, '>','sequence', context)
182
183     def stage_previous(self, cr, uid, ids, context=None):
184         """ This function computes previous stage for case from its current
185             stage using available stage for that case type
186         """
187         return self.stage_change(cr, uid, ids, '<', 'sequence desc', context)
188
189     def copy(self, cr, uid, id, default=None, context=None):
190         """ Overrides orm copy method to avoid copying messages,
191             as well as date_closed and date_open columns if they
192             exist."""
193         if default is None:
194             default = {}
195
196         if hasattr(self, '_columns'):
197             if self._columns.get('date_closed'):
198                 default.update({ 'date_closed': False, })
199             if self._columns.get('date_open'):
200                 default.update({ 'date_open': False })
201         return super(base_stage, self).copy(cr, uid, id, default, context=context)
202
203     def case_escalate(self, cr, uid, ids, context=None):
204         """ Escalates case to parent level """
205         cases = self.browse(cr, uid, ids, context=context)
206         cases[0].state # fill browse record cache, for _action having old and new values
207         for case in cases:
208             data = {'active': True}
209             if case.section_id.parent_id:
210                 data['section_id'] = case.section_id.parent_id.id
211                 if case.section_id.parent_id.change_responsible:
212                     if case.section_id.parent_id.user_id:
213                         data['user_id'] = case.section_id.parent_id.user_id.id
214             else:
215                 raise osv.except_osv(_('Error!'), _("You are already at the top level of your sales-team category.\nTherefore you cannot escalate furthermore."))
216             self.write(cr, uid, [case.id], data, context=context)
217             case.case_escalate_send_note(case.section_id.parent_id, context=context)
218         cases = self.browse(cr, uid, ids, context=context)
219         self._action(cr, uid, cases, 'escalate', context=context)
220         return True
221
222     def case_open(self, cr, uid, ids, context=None):
223         """ Opens case """
224         cases = self.browse(cr, uid, ids, context=context)
225         for case in cases:
226             data = {'active': True}
227             if case.stage_id and case.stage_id.state == 'draft':
228                 data['date_open'] = fields.datetime.now()
229             if not case.user_id:
230                 data['user_id'] = uid
231             self.case_set(cr, uid, [case.id], 'open', data, context=context)
232             self.case_open_send_note(cr, uid, [case.id], context=context)
233         return True
234
235     def case_close(self, cr, uid, ids, context=None):
236         """ Closes case """
237         self.case_set(cr, uid, ids, 'done', {'active': True, 'date_closed': fields.datetime.now()}, context=context)
238         self.case_close_send_note(cr, uid, ids, context=context)
239         return True
240
241     def case_cancel(self, cr, uid, ids, context=None):
242         """ Cancels case """
243         self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
244         self.case_cancel_send_note(cr, uid, ids, context=context)
245         return True
246
247     def case_pending(self, cr, uid, ids, context=None):
248         """ Set case as pending """
249         self.case_set(cr, uid, ids, 'pending', {'active': True}, context=context)
250         self.case_pending_send_note(cr, uid, ids, context=context)
251         return True
252
253     def case_reset(self, cr, uid, ids, context=None):
254         """ Resets case as draft """
255         self.case_set(cr, uid, ids, 'draft', {'active': True}, context=context)
256         self.case_reset_send_note(cr, uid, ids, context=context)
257         return True
258
259     def case_set(self, cr, uid, ids, new_state_name=None, values_to_update=None, new_stage_id=None, context=None):
260         """ Generic method for setting case. This methods wraps the update
261             of the record, as well as call to _action and browse_record
262             case setting to fill the cache.
263
264             :params new_state_name: the new state of the record; this method
265                                     will call ``stage_set_with_state_name``
266                                     that will find the stage matching the
267                                     new state, using the ``stage_find`` method.
268             :params new_stage_id: alternatively, you may directly give the
269                                   new stage of the record
270             :params state_name: the new value of the state, such as
271                      'draft' or 'close'.
272             :params update_values: values that will be added with the state
273                      update when writing values to the record.
274         """
275         cases = self.browse(cr, uid, ids, context=context)
276         cases[0].state # fill browse record cache, for _action having old and new values
277         # 1. update the stage
278         if new_state_name:
279             self.stage_set_with_state_name(cr, uid, cases, new_state_name, context=context)
280         elif not (new_stage_id is None):
281             self.stage_set(cr, uid, ids, new_stage_id, context=context)
282         # 2. update values
283         if values_to_update:
284             self.write(cr, uid, ids, values_to_update, context=context)
285         # 3. call _action for base action rule
286         if new_state_name:
287             self._action(cr, uid, cases, new_state_name, context=context)
288         elif not (new_stage_id is None):
289             new_state_name = self.read(cr, uid, ids, ['state'], context=context)[0]['state']
290         self._action(cr, uid, cases, new_state_name, context=context)
291         return True
292
293     def _action(self, cr, uid, cases, state_to, scrit=None, context=None):
294         if context is None:
295             context = {}
296         context['state_to'] = state_to
297         rule_obj = self.pool.get('base.action.rule')
298         if not rule_obj:
299             return True
300         model_obj = self.pool.get('ir.model')
301         model_ids = model_obj.search(cr, uid, [('model','=',self._name)], context=context)
302         rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])], context=context)
303         return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context)
304
305     def remind_partner(self, cr, uid, ids, context=None, attach=False):
306         return self.remind_user(cr, uid, ids, context, attach,
307                 destination=False)
308
309     def remind_user(self, cr, uid, ids, context=None, attach=False, destination=True):
310         if 'message_post' in self:
311             for case in self.browse(cr, uid, ids, context=context):
312                 if destination:
313                     recipient_id = case.user_id.partner_id.id
314                 else:
315                     if not case.email_from:
316                         return False
317                     recipient_id = self.pool.get('res.partner').find_or_create(cr, uid, case.email_from, context=context)
318                 
319                 body = case.description or ""
320                 for message in case.message_ids:
321                     if message.type == 'email' and message.body:
322                         body = message.body
323                         break
324                 body = self.format_body(body)
325                 attach_to_send = {}
326                 if attach:
327                     attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
328                     attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname', 'datas'])
329                     attach_to_send = dict(map(lambda x: (x['datas_fname'], x['datas'].decode('base64')), attach_to_send))
330  
331                 subject = "Reminder: [%s] %s" % (case.id, case.name)
332                 self.message_post(cr, uid, case.id, body=body,
333                     subject=subject, attachments=attach_to_send, 
334                     partner_ids=[recipient_id], context=context)
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     # ******************************
350     # Notifications
351     # ******************************
352
353     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
354         """ Default prefix for notifications. For example: "%s has been
355             <b>closed</b>.". As several models will inherit from base_stage,
356             this method returns a void string. Class using base_stage
357             will have to override this method to define the prefix they
358             want to display.
359         """
360         return ''
361
362     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
363         """ Send a notification when the stage changes. This method has
364             to be overriden, because each document will have its particular
365             behavior and/or stage model (such as project.task.type or
366             crm.case.stage).
367         """
368         return True
369
370     def case_open_send_note(self, cr, uid, ids, context=None):
371         for id in ids:
372             msg = _('%s has been <b>opened</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
373             self.message_post(cr, uid, [id], body=msg, context=context)
374         return True
375
376     def case_close_send_note(self, cr, uid, ids, context=None):
377         for id in ids:
378             msg = _('%s has been <b>closed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
379             self.message_post(cr, uid, [id], body=msg, context=context)
380         return True
381
382     def case_cancel_send_note(self, cr, uid, ids, context=None):
383         for id in ids:
384             msg = _('%s has been <b>canceled</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
385             self.message_post(cr, uid, [id], body=msg, context=context)
386         return True
387
388     def case_pending_send_note(self, cr, uid, ids, context=None):
389         for id in ids:
390             msg = _('%s is now <b>pending</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
391             self.message_post(cr, uid, [id], body=msg, context=context)
392         return True
393
394     def case_reset_send_note(self, cr, uid, ids, context=None):
395         for id in ids:
396             msg = _('%s has been <b>renewed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
397             self.message_post(cr, uid, [id], body=msg, context=context)
398         return True
399
400     def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
401         for id in ids:
402             if new_section:
403                 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)
404             else:
405                 msg = '%s has been <b>escalated</b>.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
406             self.message_post(cr, uid, [id], body=msg, context=context)
407         return True