[IMP] improve method the code
[odoo/odoo.git] / addons / project_issue / project_issue.py
1  #-*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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 base_status.base_stage import base_stage
23 from crm import crm
24 from datetime import datetime
25 from osv import fields,osv
26 from tools.translate import _
27 import binascii
28 import time
29 import tools
30 from tools import html2plaintext
31
32 class project_issue_version(osv.osv):
33     _name = "project.issue.version"
34     _order = "name desc"
35     _columns = {
36         'name': fields.char('Version Number', size=32, required=True),
37         'active': fields.boolean('Active', required=False),
38     }
39     _defaults = {
40         'active': 1,
41     }
42 project_issue_version()
43
44 _ISSUE_STATE= [('draft', 'New'), ('open', 'In Progress'), ('cancel', 'Cancelled'), ('done', 'Done'),('pending', 'Pending')]
45
46 class project_issue(base_stage, osv.osv):
47     _name = "project.issue"
48     _description = "Project Issue"
49     _order = "priority, create_date desc"
50     _inherit = ['mail.thread', 'ir.needaction_mixin']
51
52     def _get_default_project_id(self, cr, uid, context=None):
53         """ Gives default project by checking if present in the context """
54         return self._resolve_project_id_from_context(cr, uid, context=context)
55
56     def _get_default_stage_id(self, cr, uid, context=None):
57         """ Gives default stage_id """
58         project_id = self._get_default_project_id(cr, uid, context=context)
59         return self.stage_find(cr, uid, [], project_id, [('state', '=', 'draft')], context=context)
60
61     def _resolve_project_id_from_context(self, cr, uid, context=None):
62         """ Returns ID of project based on the value of 'default_project_id'
63             context key, or None if it cannot be resolved to a single
64             project.
65         """
66         if context is None:
67             context = {}
68         if type(context.get('default_project_id')) in (int, long):
69             return context.get('default_project_id')
70         if isinstance(context.get('default_project_id'), basestring):
71             project_name = context['default_project_id']
72             project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
73             if len(project_ids) == 1:
74                 return int(project_ids[0][0])
75         return None
76
77     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
78         access_rights_uid = access_rights_uid or uid
79         stage_obj = self.pool.get('project.task.type')
80         order = stage_obj._order
81         # lame hack to allow reverting search, should just work in the trivial case
82         if read_group_order == 'stage_id desc':
83             order = "%s desc" % order
84         # retrieve section_id from the context and write the domain
85         # - ('id', 'in', 'ids'): add columns that should be present
86         # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
87         # - OR ('project_ids', 'in', project_id), ('fold', '=', False) if project_id: add project columns that are not folded
88         search_domain = []
89         project_id = self._resolve_project_id_from_context(cr, uid, context=context)
90         if project_id:
91             search_domain += ['|', ('project_ids', '=', project_id)]
92         search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
93         # perform search
94         stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
95         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
96         # restore order of the search
97         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
98
99         fold = {}
100         for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
101             fold[stage.id] = stage.fold or False
102         return result, fold
103
104     def _compute_day(self, cr, uid, ids, fields, args, context=None):
105         """
106         @param cr: the current row, from the database cursor,
107         @param uid: the current user’s ID for security checks,
108         @param ids: List of Openday’s IDs
109         @return: difference between current date and log date
110         @param context: A standard dictionary for contextual values
111         """
112         cal_obj = self.pool.get('resource.calendar')
113         res_obj = self.pool.get('resource.resource')
114
115         res = {}
116         for issue in self.browse(cr, uid, ids, context=context):
117             res[issue.id] = {}
118             for field in fields:
119                 duration = 0
120                 ans = False
121                 hours = 0
122
123                 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
124                 if field in ['working_hours_open','day_open']:
125                     if issue.date_open:
126                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
127                         ans = date_open - date_create
128                         date_until = issue.date_open
129                         #Calculating no. of working hours to open the issue
130                         if issue.project_id.resource_calendar_id:
131                             hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
132                                                            date_create,
133                                                            date_open)
134                 elif field in ['working_hours_close','day_close']:
135                     if issue.date_closed:
136                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
137                         date_until = issue.date_closed
138                         ans = date_close - date_create
139                         #Calculating no. of working hours to close the issue
140                         if issue.project_id.resource_calendar_id:
141                             hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
142                                date_create,
143                                date_close)
144                 elif field in ['days_since_creation']:
145                     if issue.create_date:
146                         days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
147                         res[issue.id][field] = days_since_creation.days
148                     continue
149
150                 elif field in ['inactivity_days']:
151                     res[issue.id][field] = 0
152                     if issue.date_action_last:
153                         inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
154                         res[issue.id][field] = inactive_days.days
155                     continue
156                 if ans:
157                     resource_id = False
158                     if issue.user_id:
159                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
160                         if resource_ids and len(resource_ids):
161                             resource_id = resource_ids[0]
162                     duration = float(ans.days)
163                     if issue.project_id and issue.project_id.resource_calendar_id:
164                         duration = float(ans.days) * 24
165
166                         new_dates = cal_obj.interval_min_get(cr, uid,
167                                                              issue.project_id.resource_calendar_id.id,
168                                                              date_create,
169                                                              duration, resource=resource_id)
170                         no_days = []
171                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
172                         for in_time, out_time in new_dates:
173                             if in_time.date not in no_days:
174                                 no_days.append(in_time.date)
175                             if out_time > date_until:
176                                 break
177                         duration = len(no_days)
178
179                 if field in ['working_hours_open','working_hours_close']:
180                     res[issue.id][field] = hours
181                 else:
182                     res[issue.id][field] = abs(float(duration))
183
184         return res
185
186     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
187         task_pool = self.pool.get('project.task')
188         res = {}
189         for issue in self.browse(cr, uid, ids, context=context):
190             progress = 0.0
191             if issue.task_id:
192                 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
193             res[issue.id] = {'progress' : progress}
194         return res
195
196     def on_change_project(self, cr, uid, ids, project_id, context=None):
197         return {}
198
199     def _get_issue_task(self, cr, uid, ids, context=None):
200         issues = []
201         issue_pool = self.pool.get('project.issue')
202         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
203             issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
204         return issues
205
206     def _get_issue_work(self, cr, uid, ids, context=None):
207         issues = []
208         issue_pool = self.pool.get('project.issue')
209         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
210             if work.task_id:
211                 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
212         return issues
213
214     _columns = {
215         'id': fields.integer('ID', readonly=True),
216         'name': fields.char('Issue', size=128, required=True),
217         'active': fields.boolean('Active', required=False),
218         'create_date': fields.datetime('Creation Date', readonly=True,select=True),
219         'write_date': fields.datetime('Update Date', readonly=True),
220         'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
221                                                multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
222         'date_deadline': fields.date('Deadline'),
223         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
224                         select=True, help='Sales team to which Case belongs to.\
225                              Define Responsible user and Email account for mail gateway.'),
226         'partner_id': fields.many2one('res.partner', 'Contact', select=1),
227         'company_id': fields.many2one('res.company', 'Company'),
228         'description': fields.text('Description'),
229         'state': fields.related('stage_id', 'state', type="selection", store=True,
230                 selection=_ISSUE_STATE, string="Status", readonly=True,
231                 help='The status is set to \'Draft\', when a case is created.\
232                       If the case is in progress the status is set to \'Open\'.\
233                       When the case is over, the status is set to \'Done\'.\
234                       If the case needs to be reviewed then the status is \
235                       set to \'Pending\'.'),
236         'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
237         'email_cc': fields.char('Watchers Emails', size=256, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
238         'date_open': fields.datetime('Opened', readonly=True,select=True),
239         # Project Issue fields
240         'date_closed': fields.datetime('Closed', readonly=True,select=True),
241         'date': fields.datetime('Date'),
242         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
243         'categ_ids': fields.many2many('project.category', string='Tags'),
244         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
245         'version_id': fields.many2one('project.issue.version', 'Version'),
246         'stage_id': fields.many2one ('project.task.type', 'Stage',
247                         domain="['&', ('fold', '=', False), '|', ('project_ids', '=', project_id), ('case_default', '=', True)]"),
248         'project_id':fields.many2one('project.project', 'Project'),
249         'duration': fields.float('Duration'),
250         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
251         'day_open': fields.function(_compute_day, string='Days to Open', \
252                                 multi='compute_day', type="float", store=True),
253         'day_close': fields.function(_compute_day, string='Days to Close', \
254                                 multi='compute_day', type="float", store=True),
255         'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
256         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
257                                 multi='compute_day', type="float", store=True),
258         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
259                                 multi='compute_day', type="float", store=True),
260         'inactivity_days': fields.function(_compute_day, string='Days since last action', \
261                                 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
262         'color': fields.integer('Color Index'),
263         'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
264         'date_action_last': fields.datetime('Last Action', readonly=1),
265         'date_action_next': fields.datetime('Next Action', readonly=1),
266         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
267             store = {
268                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
269                 'project.task': (_get_issue_task, ['progress'], 10),
270                 'project.task.work': (_get_issue_work, ['hours'], 10),
271             }),
272     }
273
274     _defaults = {
275         'active': 1,
276         'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
277         'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
278         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
279         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
280         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
281         'priority': crm.AVAILABLE_PRIORITIES[2][0],
282     }
283
284     _group_by_full = {
285         'stage_id': _read_group_stage_ids
286     }
287
288     def set_priority(self, cr, uid, ids, priority, *args):
289         """Set lead priority
290         """
291         return self.write(cr, uid, ids, {'priority' : priority})
292
293     def set_high_priority(self, cr, uid, ids, *args):
294         """Set lead priority to high
295         """
296         return self.set_priority(cr, uid, ids, '1')
297
298     def set_normal_priority(self, cr, uid, ids, *args):
299         """Set lead priority to normal
300         """
301         return self.set_priority(cr, uid, ids, '3')
302
303     def convert_issue_task(self, cr, uid, ids, context=None):
304         if context is None:
305             context = {}
306
307         case_obj = self.pool.get('project.issue')
308         data_obj = self.pool.get('ir.model.data')
309         task_obj = self.pool.get('project.task')
310
311         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
312         res = data_obj.read(cr, uid, result, ['res_id'])
313         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
314         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
315         if id2:
316             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
317         if id3:
318             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
319
320         for bug in case_obj.browse(cr, uid, ids, context=context):
321             new_task_id = task_obj.create(cr, uid, {
322                 'name': bug.name,
323                 'partner_id': bug.partner_id.id,
324                 'description':bug.description,
325                 'date_deadline': bug.date,
326                 'project_id': bug.project_id.id,
327                 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
328                 'priority': str(int(bug.priority) - 1),
329                 'user_id': bug.user_id.id,
330                 'planned_hours': 0.0,
331             })
332             vals = {
333                 'task_id': new_task_id,
334                 'stage_id': self.stage_find(cr, uid, [bug], bug.project_id.id, [('state', '=', 'pending')], context=context),
335             }
336             self.convert_to_task_send_note(cr, uid, [bug.id], context=context)
337             case_obj.write(cr, uid, [bug.id], vals, context=context)
338             self.case_pending_send_note(cr, uid, [bug.id], context=context)
339
340         return  {
341             'name': _('Tasks'),
342             'view_type': 'form',
343             'view_mode': 'form,tree',
344             'res_model': 'project.task',
345             'res_id': int(new_task_id),
346             'view_id': False,
347             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
348             'type': 'ir.actions.act_window',
349             'search_view_id': res['res_id'],
350             'nodestroy': True
351         }
352
353     def copy(self, cr, uid, id, default=None, context=None):
354         issue = self.read(cr, uid, id, ['name'], context=context)
355         if not default:
356             default = {}
357         default = default.copy()
358         default.update(name=_('%s (copy)') % (issue['name']))
359         return super(project_issue, self).copy(cr, uid, id, default=default,
360                 context=context)
361
362     def _subscribe_project_followers_to_issue(self, cr, uid, task_id, context=None):
363         """ TDE note: not the best way to do this, we could override _get_followers
364             of issue, and perform a better mapping of subtypes than a mapping
365             based on names.
366             However we will keep this implementation, maybe to be refactored
367             in 7.1 of future versions. """
368         # task followers are project followers, with matching subtypes
369         task_record = self.browse(cr, uid, task_id, context=context)
370         subtype_obj = self.pool.get('mail.message.subtype')
371         follower_obj = self.pool.get('mail.followers')
372         if task_record.project_id:
373             # create mapping
374             task_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('res_model', '=', self._name)], context=context)
375             task_subtypes = subtype_obj.browse(cr, uid, task_subtype_ids, context=context)
376             # fetch subscriptions
377             follower_ids = follower_obj.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', task_record.project_id.id)], context=context)
378             # copy followers
379             for follower in follower_obj.browse(cr, uid, follower_ids, context=context):
380                 if not follower.subtype_ids:
381                     continue
382                 project_subtype_names = [project_subtype.name for project_subtype in follower.subtype_ids]
383                 task_subtype_ids = [task_subtype.id for task_subtype in task_subtypes if task_subtype.name in project_subtype_names]
384                 self.message_subscribe(cr, uid, [task_id], [follower.partner_id.id],
385                     subtype_ids=task_subtype_ids, context=context)
386
387     def write(self, cr, uid, ids, vals, context=None):
388         #Update last action date every time the user change the stage, the state or send a new email
389         logged_fields = ['stage_id', 'state', 'message_ids']
390         if any([field in vals for field in logged_fields]):
391             vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
392
393         # subscribe new project followers to the issue
394         if vals.get('project_id'):
395             for id in ids:
396                 self._subscribe_project_followers_to_issue(cr, uid, id, context=context)
397         st = self.pool.get('project.task.type').browse(cr, uid, vals.get('stage_id'), context=context);
398         if st.state  =='open':
399             self.issue_start_send_note(cr, uid, ids, context=context)
400         return super(project_issue, self).write(cr, uid, ids, vals, context)
401     
402     def issue_start_send_note(self, cr, uid, ids, context=None):
403         return self.message_post(cr, uid, ids, body=_("Issue has been <b>started</b>."), subtype="mt_issue_change", context=context)
404
405     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
406         if not task_id:
407             return {'value': {}}
408         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
409         return {'value': {'user_id': task.user_id.id, }}
410
411     def case_reset(self, cr, uid, ids, context=None):
412         """Resets case as draft
413         """
414         res = super(project_issue, self).case_reset(cr, uid, ids, context)
415         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
416         return res
417
418     def create(self, cr, uid, vals, context=None):
419         obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
420
421         # subscribe project follower to the issue
422         self._subscribe_project_followers_to_issue(cr, uid, obj_id, context=context)
423         self.create_send_note(cr, uid, [obj_id], context=context)
424
425         return obj_id
426
427     # -------------------------------------------------------
428     # Stage management
429     # -------------------------------------------------------
430
431     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
432         """ Override of the base.stage method
433             Parameter of the stage search taken from the issue:
434             - type: stage type must be the same or 'both'
435             - section_id: if set, stages must belong to this section or
436               be a default case
437         """
438         if isinstance(cases, (int, long)):
439             cases = self.browse(cr, uid, cases, context=context)
440         # collect all section_ids
441         section_ids = []
442         if section_id:
443             section_ids.append(section_id)
444         for task in cases:
445             if task.project_id:
446                 section_ids.append(task.project_id.id)
447         # OR all section_ids and OR with case_default
448         search_domain = []
449         if section_ids:
450             search_domain += [('|')] * len(section_ids)
451             for section_id in section_ids:
452                 search_domain.append(('project_ids', '=', section_id))
453         search_domain.append(('case_default', '=', True))
454         # AND with the domain in parameter
455         search_domain += list(domain)
456         # perform search, return the first found
457         stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
458         if stage_ids:
459             return stage_ids[0]
460         return False
461
462     def case_cancel(self, cr, uid, ids, context=None):
463         """ Cancels case """
464         self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
465         return True
466
467     def case_escalate(self, cr, uid, ids, context=None):
468         cases = self.browse(cr, uid, ids)
469         for case in cases:
470             data = {}
471             if case.project_id.project_escalation_id:
472                 data['project_id'] = case.project_id.project_escalation_id.id
473                 if case.project_id.project_escalation_id.user_id:
474                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
475                 if case.task_id:
476                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
477             else:
478                 raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
479             self.case_set(cr, uid, ids, 'draft', data, context=context)
480             self.case_escalate_send_note(cr, uid, [case.id], context=context)
481         return True
482
483     # -------------------------------------------------------
484     # Mail gateway
485     # -------------------------------------------------------
486
487     def message_new(self, cr, uid, msg, custom_values=None, context=None):
488         """ Overrides mail_thread message_new that is called by the mailgateway
489             through message_process.
490             This override updates the document according to the email.
491         """
492         if custom_values is None: custom_values = {}
493         if context is None: context = {}
494         context['state_to'] = 'draft'
495
496         desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
497
498         custom_values.update({
499             'name':  msg.get('subject') or _("No Subject"),
500             'description': desc,
501             'email_from': msg.get('from'),
502             'email_cc': msg.get('cc'),
503             'user_id': False,
504         })
505         if  msg.get('priority'):
506             custom_values['priority'] =  msg.get('priority')
507
508         res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
509         # self.convert_to_bug(cr, uid, [res_id], context=context)
510         return res_id
511
512     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
513         """ Overrides mail_thread message_update that is called by the mailgateway
514             through message_process.
515             This method updates the document according to the email.
516         """
517         if isinstance(ids, (str, int, long)):
518             ids = [ids]
519         if update_vals is None: update_vals = {}
520
521         # Update doc values according to the message
522         if msg.get('priority'):
523             update_vals['priority'] = msg.get('priority')
524         # Parse 'body' to find values to update
525         maps = {
526             'cost': 'planned_cost',
527             'revenue': 'planned_revenue',
528             'probability': 'probability',
529         }
530         for line in msg.get('body', '').split('\n'):
531             line = line.strip()
532             res = tools.misc.command_re.match(line)
533             if res and maps.get(res.group(1).lower(), False):
534                 key = maps.get(res.group(1).lower())
535                 update_vals[key] = res.group(2).lower()
536
537         return super(project_issue, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
538
539     # -------------------------------------------------------
540     # OpenChatter methods and notifications
541     # -------------------------------------------------------
542     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
543         """ Override of the (void) default notification method. """
544         stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
545         return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), subtype="mt_issue_new", context=context)
546     
547     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
548         """ Override of default prefix for notifications. """
549         return 'Project issue'
550
551     def convert_to_task_send_note(self, cr, uid, ids, context=None):
552         message = _("Project issue <b>converted</b> to task.")
553         return self.message_post(cr, uid, ids, body=message, context=context)
554     
555     def create_send_note(self, cr, uid, ids, context=None):
556         message = _("Project issue <b>created</b>.")
557         return self.message_post(cr, uid, ids, body=message, subtype="mt_issue_new", context=context)
558
559     def case_escalate_send_note(self, cr, uid, ids, context=None):
560         for obj in self.browse(cr, uid, ids, context=context):
561             if obj.project_id:
562                 message = _("<b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
563                 obj.message_post(body=message)
564             else:
565                 message = _("<b>escalated</b>.")
566                 obj.message_post(body=message)
567         return True
568
569 project_issue()
570
571 class project(osv.osv):
572     _inherit = "project.project"
573
574     def _get_alias_models(self, cr, uid, context=None):
575         return [('project.task', "Tasks"), ("project.issue", "Issues")]
576
577     def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
578         res = dict.fromkeys(ids, 0)
579         issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
580         for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
581             res[issue.project_id.id] += 1
582         return res
583
584     _columns = {
585         'project_escalation_id' : fields.many2one('project.project','Project Escalation', help='If any issue is escalated from the current Project, it will be listed under the project selected here.', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
586         'issue_count': fields.function(_issue_count, type='integer'),
587     }
588
589     def _check_escalation(self, cr, uid, ids, context=None):
590         project_obj = self.browse(cr, uid, ids[0], context=context)
591         if project_obj.project_escalation_id:
592             if project_obj.project_escalation_id.id == project_obj.id:
593                 return False
594         return True
595
596     _constraints = [
597         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
598     ]
599
600 project()
601
602 class account_analytic_account(osv.osv):
603     _inherit = 'account.analytic.account'
604     _description = 'Analytic Account'
605
606     _columns = {
607         'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
608     }
609
610     def on_change_template(self, cr, uid, ids, template_id, context=None):
611         res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
612         if template_id and 'value' in res:
613             template = self.browse(cr, uid, template_id, context=context)
614             res['value']['use_issues'] = template.use_issues
615         return res
616
617     def _trigger_project_creation(self, cr, uid, vals, context=None):
618         if context is None: context = {}
619         res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
620         return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
621
622 account_analytic_account()
623
624 class project_project(osv.osv):
625     _inherit = 'project.project'
626     _defaults = {
627         'use_issues': True
628     }
629
630 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: