1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
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.
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.
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/>.
20 ##############################################################################
22 from osv import fields, osv
23 from tools.translate import _
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)
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
44 if not context or not context.get('portal'):
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
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
57 if not context or not context.get('portal'):
59 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
60 return user.user_email
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
68 if not context or not context.get('portal'):
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
77 data = {'value': {'email_from': False, 'phone':False}}
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']
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
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}
97 def _get_default_section_id(self, cr, uid, context=None):
98 """ Gives default section """
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)
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
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
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
128 if isinstance(cases, (int, long)):
129 cases = self.browse(cr, uid, cases, context=context)
131 stage_id = self.stage_find(cr, uid, [case], None, [('state', '=', state_name)], context=context)
133 self.stage_set(cr, uid, [case.id], stage_id, context=context)
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.
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)
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')].
157 for case in self.browse(cr, uid, ids, context=context):
160 seq = case.stage_id.sequence or 0
162 next_stage_id = self.stage_find(cr, uid, [case], None, [('sequence', op, seq)],order, context=context)
164 return self.stage_set(cr, uid, [case.id], next_stage_id, context=context)
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
171 return self.stage_change(cr, uid, ids, '>','sequence', context)
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
177 return self.stage_change(cr, uid, ids, '<', 'sequence desc', context)
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
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)
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
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
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)
212 def case_open(self, cr, uid, ids, context=None):
214 cases = self.browse(cr, uid, ids, context=context)
216 data = {'active': True}
217 if case.stage_id and case.stage_id.state == 'draft':
218 data['date_open'] = fields.datetime.now()
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)
225 def case_close(self, cr, uid, ids, context=None):
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)
231 def case_cancel(self, cr, uid, ids, context=None):
233 self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
234 self.case_cancel_send_note(cr, uid, ids, context=context)
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)
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)
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.
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
262 :params update_values: values that will be added with the state
263 update when writing values to the record.
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
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)
274 self.write(cr, uid, ids, values_to_update, context=context)
275 # 3. call _action for base action rule
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)
283 def _action(self, cr, uid, cases, state_to, scrit=None, context=None):
286 context['state_to'] = state_to
287 rule_obj = self.pool.get('base.action.rule')
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)
295 def remind_partner(self, cr, uid, ids, context=None, attach=False):
296 return self.remind_user(cr, uid, ids, context, attach,
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:
304 if not case.user_id.user_email:
306 if destination and case.section_id.user_id:
307 case_email = case.section_id.user_id.user_email
309 case_email = case.user_id.user_email
312 dest = case.user_id.user_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
320 src, dest = dest, case.email_from
321 if body and case.user_id.signature:
323 body += '\n\n%s' % (case.user_id.signature)
325 body = '\n\n%s' % (case.user_id.signature)
327 body = self.format_body(body)
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))
337 subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
338 mail_message.schedule_with_attach(cr, uid,
344 reply_to=case.section_id.reply_to,
346 attachments=attach_to_send,
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.
357 def format_body(self, body):
358 return self.pool.get('base.action.rule').format_body(body)
360 def format_mail(self, obj, body):
361 return self.pool.get('base.action.rule').format_mail(obj, body)
363 def message_thread_followers(self, cr, uid, ids, context=None):
365 for case in self.browse(cr, uid, ids, context=context):
368 l.append(case.email_cc)
369 if case.user_id and case.user_id.user_email:
370 l.append(case.user_id.user_email)
374 # ******************************
376 # ******************************
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
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
395 def case_open_send_note(self, cr, uid, ids, context=None):
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)
401 def case_close_send_note(self, cr, uid, ids, context=None):
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)
407 def case_cancel_send_note(self, cr, uid, ids, context=None):
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)
413 def case_pending_send_note(self, cr, uid, ids, context=None):
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)
419 def case_reset_send_note(self, cr, uid, ids, context=None):
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)
425 def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
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)
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)