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)
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, 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
77 data = {'value': {'email_from': False, 'phone':False}}
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,
90 fields = self.fields_get(cr, uid, context=context or {})
91 for key in data['value'].keys():
93 del data['value'][key]
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
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}
107 def _get_default_section_id(self, cr, uid, context=None):
108 """ Gives default section """
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)
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
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
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
138 if isinstance(cases, (int, long)):
139 cases = self.browse(cr, uid, cases, context=context)
141 stage_id = self.stage_find(cr, uid, [case], None, [('state', '=', state_name)], context=context)
143 self.stage_set(cr, uid, [case.id], stage_id, context=context)
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.
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)
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')].
167 for case in self.browse(cr, uid, ids, context=context):
170 seq = case.stage_id.sequence or 0
172 next_stage_id = self.stage_find(cr, uid, [case], None, [('sequence', op, seq)],order, context=context)
174 return self.stage_set(cr, uid, [case.id], next_stage_id, context=context)
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
181 return self.stage_change(cr, uid, ids, '>','sequence', context)
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
187 return self.stage_change(cr, uid, ids, '<', 'sequence desc', context)
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
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)
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
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
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)
222 def case_open(self, cr, uid, ids, context=None):
224 cases = self.browse(cr, uid, ids, context=context)
226 data = {'active': True}
227 if case.stage_id and case.stage_id.state == 'draft':
228 data['date_open'] = fields.datetime.now()
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)
235 def case_close(self, cr, uid, ids, context=None):
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)
241 def case_cancel(self, cr, uid, ids, context=None):
243 self.case_set(cr, uid, ids, 'cancel', {'active': True}, context=context)
244 self.case_cancel_send_note(cr, uid, ids, context=context)
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)
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)
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.
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
272 :params update_values: values that will be added with the state
273 update when writing values to the record.
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
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)
284 self.write(cr, uid, ids, values_to_update, context=context)
285 # 3. call _action for base action rule
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)
293 def _action(self, cr, uid, cases, state_to, scrit=None, context=None):
296 context['state_to'] = state_to
297 rule_obj = self.pool.get('base.action.rule')
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)
305 def remind_partner(self, cr, uid, ids, context=None, attach=False):
306 return self.remind_user(cr, uid, ids, context, attach,
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):
313 recipient_id = case.user_id.partner_id.id
315 if not case.email_from:
317 recipient_id = self.pool.get('res.partner').find_or_create(cr, uid, case.email_from, context=context)
319 body = case.description or ""
320 for message in case.message_ids:
321 if message.type == 'email' and message.body:
324 body = self.format_body(body)
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))
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)
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.
343 def format_body(self, body):
344 return self.pool.get('base.action.rule').format_body(body)
346 def format_mail(self, obj, body):
347 return self.pool.get('base.action.rule').format_mail(obj, body)
349 # ******************************
351 # ******************************
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
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
370 def case_open_send_note(self, cr, uid, ids, context=None):
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)
376 def case_close_send_note(self, cr, uid, ids, context=None):
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)
382 def case_cancel_send_note(self, cr, uid, ids, context=None):
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)
388 def case_pending_send_note(self, cr, uid, ids, context=None):
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)
394 def case_reset_send_note(self, cr, uid, ids, context=None):
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)
400 def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
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)
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)