[IMP]hr_all: fix some traceback issue related to hr module
[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
31 class project_issue_version(osv.osv):
32     _name = "project.issue.version"
33     _order = "name desc"
34     _columns = {
35         'name': fields.char('Version Number', size=32, required=True),
36         'active': fields.boolean('Active', required=False),
37     }
38     _defaults = {
39         'active': 1,
40     }
41 project_issue_version()
42
43 _ISSUE_STATE= [('draft', 'New'), ('open', 'In Progress'), ('cancel', 'Cancelled'), ('done', 'Done'),('pending', 'Pending')]
44
45 class project_issue(base_stage, osv.osv):
46     _name = "project.issue"
47     _description = "Project Issue"
48     _order = "priority, create_date desc"
49     _inherit = ['ir.needaction_mixin', 'mail.thread']
50     _mail_compose_message = True
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), ('fold', '=', False)]
92         search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
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         return result
99
100     def _compute_day(self, cr, uid, ids, fields, args, context=None):
101         """
102         @param cr: the current row, from the database cursor,
103         @param uid: the current user’s ID for security checks,
104         @param ids: List of Openday’s IDs
105         @return: difference between current date and log date
106         @param context: A standard dictionary for contextual values
107         """
108         cal_obj = self.pool.get('resource.calendar')
109         res_obj = self.pool.get('resource.resource')
110
111         res = {}
112         for issue in self.browse(cr, uid, ids, context=context):
113             res[issue.id] = {}
114             for field in fields:
115                 duration = 0
116                 ans = False
117                 hours = 0
118
119                 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
120                 if field in ['working_hours_open','day_open']:
121                     if issue.date_open:
122                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
123                         ans = date_open - date_create
124                         date_until = issue.date_open
125                         #Calculating no. of working hours to open the issue
126                         if issue.project_id.resource_calendar_id:
127                             hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
128                                                            date_create,
129                                                            date_open)
130                 elif field in ['working_hours_close','day_close']:
131                     if issue.date_closed:
132                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
133                         date_until = issue.date_closed
134                         ans = date_close - date_create
135                         #Calculating no. of working hours to close the issue
136                         if issue.project_id.resource_calendar_id:
137                             hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
138                                date_create,
139                                date_close)
140                 elif field in ['days_since_creation']:
141                     if issue.create_date:
142                         days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
143                         res[issue.id][field] = days_since_creation.days
144                     continue
145
146                 elif field in ['inactivity_days']:
147                     res[issue.id][field] = 0
148                     if issue.date_action_last:
149                         inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
150                         res[issue.id][field] = inactive_days.days
151                     continue
152                 if ans:
153                     resource_id = False
154                     if issue.user_id:
155                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
156                         if resource_ids and len(resource_ids):
157                             resource_id = resource_ids[0]
158                     duration = float(ans.days)
159                     if issue.project_id and issue.project_id.resource_calendar_id:
160                         duration = float(ans.days) * 24
161
162                         new_dates = cal_obj.interval_min_get(cr, uid,
163                                                              issue.project_id.resource_calendar_id.id,
164                                                              date_create,
165                                                              duration, resource=resource_id)
166                         no_days = []
167                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
168                         for in_time, out_time in new_dates:
169                             if in_time.date not in no_days:
170                                 no_days.append(in_time.date)
171                             if out_time > date_until:
172                                 break
173                         duration = len(no_days)
174
175                 if field in ['working_hours_open','working_hours_close']:
176                     res[issue.id][field] = hours
177                 else:
178                     res[issue.id][field] = abs(float(duration))
179
180         return res
181
182     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
183         task_pool = self.pool.get('project.task')
184         res = {}
185         for issue in self.browse(cr, uid, ids, context=context):
186             progress = 0.0
187             if issue.task_id:
188                 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
189             res[issue.id] = {'progress' : progress}
190         return res
191
192     def on_change_project(self, cr, uid, ids, project_id, context=None):
193         return {}
194
195     def _get_issue_task(self, cr, uid, ids, context=None):
196         issues = []
197         issue_pool = self.pool.get('project.issue')
198         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
199             issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
200         return issues
201
202     def _get_issue_work(self, cr, uid, ids, context=None):
203         issues = []
204         issue_pool = self.pool.get('project.issue')
205         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
206             if work.task_id:
207                 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
208         return issues
209
210     _columns = {
211         'id': fields.integer('ID', readonly=True),
212         'name': fields.char('Issue', size=128, required=True),
213         'active': fields.boolean('Active', required=False),
214         'create_date': fields.datetime('Creation Date', readonly=True,select=True),
215         'write_date': fields.datetime('Update Date', readonly=True),
216         'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
217                                                multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
218         'date_deadline': fields.date('Deadline'),
219         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
220                         select=True, help='Sales team to which Case belongs to.\
221                              Define Responsible user and Email account for mail gateway.'),
222         'partner_id': fields.many2one('res.partner', 'Partner', select=1),
223         'company_id': fields.many2one('res.company', 'Company'),
224         'description': fields.text('Description'),
225         'state': fields.related('stage_id', 'state', type="selection", store=True,
226                 selection=_ISSUE_STATE, string="State", readonly=True,
227                 help='The state is set to \'Draft\', when a case is created.\
228                       If the case is in progress the state is set to \'Open\'.\
229                       When the case is over, the state is set to \'Done\'.\
230                       If the case needs to be reviewed then the state is \
231                       set to \'Pending\'.'),
232         'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
233         '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"),
234         'date_open': fields.datetime('Opened', readonly=True,select=True),
235         # Project Issue fields
236         'date_closed': fields.datetime('Closed', readonly=True,select=True),
237         'date': fields.datetime('Date'),
238         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
239         'categ_id': fields.many2one('crm.case.categ', 'Category', domain="[('object_id.model', '=', 'crm.project.bug')]"),
240         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
241         'version_id': fields.many2one('project.issue.version', 'Version'),
242         'stage_id': fields.many2one ('project.task.type', 'Stage',
243                         domain="['|', ('project_ids', '=', project_id), ('case_default', '=', True)]"),
244         'project_id':fields.many2one('project.project', 'Project'),
245         'duration': fields.float('Duration'),
246         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
247         'day_open': fields.function(_compute_day, string='Days to Open', \
248                                 multi='compute_day', type="float", store=True),
249         'day_close': fields.function(_compute_day, string='Days to Close', \
250                                 multi='compute_day', type="float", store=True),
251         'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
252         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
253                                 multi='compute_day', type="float", store=True),
254         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
255                                 multi='compute_day', type="float", store=True),
256         'inactivity_days': fields.function(_compute_day, string='Days since last action', \
257                                 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
258         'color': fields.integer('Color Index'),
259         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
260         'date_action_last': fields.datetime('Last Action', readonly=1),
261         'date_action_next': fields.datetime('Next Action', readonly=1),
262         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
263             store = {
264                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
265                 'project.task': (_get_issue_task, ['progress'], 10),
266                 'project.task.work': (_get_issue_work, ['hours'], 10),
267             }),
268     }
269
270     def on_change_project(self, cr, uid, ids, project_id, context=None):
271         return {}
272
273     _defaults = {
274         'active': 1,
275         'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
276         'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
277         'state': 'draft',
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         'categ_id' : lambda *a: False,
283          }
284
285     _group_by_full = {
286         'stage_id': _read_group_stage_ids
287     }
288
289     def set_priority(self, cr, uid, ids, priority, *args):
290         """Set lead priority
291         """
292         return self.write(cr, uid, ids, {'priority' : priority})
293
294     def set_high_priority(self, cr, uid, ids, *args):
295         """Set lead priority to high
296         """
297         return self.set_priority(cr, uid, ids, '1')
298
299     def set_normal_priority(self, cr, uid, ids, *args):
300         """Set lead priority to normal
301         """
302         return self.set_priority(cr, uid, ids, '3')
303
304     def convert_issue_task(self, cr, uid, ids, context=None):
305         if context is None:
306             context = {}
307         
308         case_obj = self.pool.get('project.issue')
309         data_obj = self.pool.get('ir.model.data')
310         task_obj = self.pool.get('project.task')
311         
312         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
313         res = data_obj.read(cr, uid, result, ['res_id'])
314         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
315         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
316         if id2:
317             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
318         if id3:
319             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
320
321         for bug in case_obj.browse(cr, uid, ids, context=context):
322             new_task_id = task_obj.create(cr, uid, {
323                 'name': bug.name,
324                 'partner_id': bug.partner_id.id,
325                 'description':bug.description,
326                 'date_deadline': bug.date,
327                 'project_id': bug.project_id.id,
328                 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
329                 'priority': str(int(bug.priority) - 1),
330                 'user_id': bug.user_id.id,
331                 'planned_hours': 0.0,
332             })
333             vals = {
334                 'task_id': new_task_id,
335                 'state':'pending'
336             }
337             self.convert_to_task_send_note(cr, uid, [bug.id], context=context)
338             case_obj.write(cr, uid, [bug.id], vals, context=context)
339             self.case_pending_send_note(cr, uid, [bug.id], context=context)
340
341         return  {
342             'name': _('Tasks'),
343             'view_type': 'form',
344             'view_mode': 'form,tree',
345             'res_model': 'project.task',
346             'res_id': int(new_task_id),
347             'view_id': False,
348             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
349             'type': 'ir.actions.act_window',
350             'search_view_id': res['res_id'],
351             'nodestroy': True
352         }
353
354
355     def _convert(self, cr, uid, ids, xml_id, context=None):
356         data_obj = self.pool.get('ir.model.data')
357         id2 = data_obj._get_id(cr, uid, 'project_issue', xml_id)
358         categ_id = False
359         if id2:
360             categ_id = data_obj.browse(cr, uid, id2, context=context).res_id
361         if categ_id:
362             self.write(cr, uid, ids, {'categ_id': categ_id})
363         return True
364
365     def convert_to_feature(self, cr, uid, ids, context=None):
366         return self._convert(cr, uid, ids, 'feature_request_categ', context=context)
367
368     def convert_to_bug(self, cr, uid, ids, context=None):
369         return self._convert(cr, uid, ids, 'bug_categ', context=context)
370
371     def copy(self, cr, uid, id, default=None, context=None):
372         issue = self.read(cr, uid, id, ['name'], context=context)
373         if not default:
374             default = {}
375         default = default.copy()
376         default['name'] = issue['name'] + _(' (copy)')
377         return super(project_issue, self).copy(cr, uid, id, default=default,
378                 context=context)
379     
380     def write(self, cr, uid, ids, vals, context=None):
381         #Update last action date every time the user change the stage, the state or send a new email
382         logged_fields = ['stage_id', 'state', 'message_ids']
383         if any([field in vals for field in logged_fields]):
384             vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
385         return super(project_issue, self).write(cr, uid, ids, vals, context)
386
387     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
388         result = {}
389         if not task_id:
390             return {'value':{}}
391         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
392         return {'value':{'user_id': task.user_id.id,}}
393
394     def case_reset(self, cr, uid, ids, context=None):
395         """Resets case as draft
396         """
397         res = super(project_issue, self).case_reset(cr, uid, ids, context)
398         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
399         return res
400
401     def create(self, cr, uid, vals, context=None):
402         obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
403         self.create_send_note(cr, uid, [obj_id], context=context)
404         return obj_id
405
406     # -------------------------------------------------------
407     # Stage management
408     # -------------------------------------------------------
409
410     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
411         """ Override of the base.stage method
412             Parameter of the stage search taken from the issue:
413             - type: stage type must be the same or 'both'
414             - section_id: if set, stages must belong to this section or
415               be a default case
416         """
417         if isinstance(cases, (int, long)):
418             cases = self.browse(cr, uid, cases, context=context)
419         # collect all section_ids
420         section_ids = []
421         if section_id:
422             section_ids.append(section_id)
423         for task in cases:
424             if task.project_id:
425                 section_ids.append(task.project_id.id)
426         # OR all section_ids and OR with case_default
427         search_domain = []
428         if section_ids:
429             search_domain += [('|')] * len(section_ids)
430             for section_id in section_ids:
431                 search_domain.append(('project_ids', '=', section_id))
432         search_domain.append(('case_default', '=', True))
433         # AND with the domain in parameter
434         search_domain += list(domain)
435         # perform search, return the first found
436         stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
437         if stage_ids:
438             return stage_ids[0]
439         return False
440
441     def case_cancel(self, cr, uid, ids, context=None):
442         """ Cancels case """
443         self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
444         self.case_cancel_send_note(cr, uid, ids, context=context)
445         return True
446
447     def case_escalate(self, cr, uid, ids, context=None):
448         cases = self.browse(cr, uid, ids)
449         for case in cases:
450             data = {}
451             if case.project_id.project_escalation_id:
452                 data['project_id'] = case.project_id.project_escalation_id.id
453                 if case.project_id.project_escalation_id.user_id:
454                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
455                 if case.task_id:
456                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
457             else:
458                 raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
459             self.case_set(cr, uid, ids, 'draft', data, context=context)
460             self.case_escalate_send_note(cr, uid, [case.id], context=context)
461         return True
462
463     # -------------------------------------------------------
464     # Mail gateway
465     # -------------------------------------------------------
466     
467     def message_new(self, cr, uid, msg, custom_values=None, context=None):
468         """ Overrides mail_thread message_new that is called by the mailgateway
469             through message_process.
470             This override updates the document according to the email.
471         """
472         if custom_values is None: custom_values = {}
473         if context is None: context = {}
474         context['state_to'] = 'draft'
475
476         custom_values.update({
477             'name':  msg.get('subject') or _("No Subject"),
478             'description': msg.get('body_text'),
479             'email_from': msg.get('from'),
480             'email_cc': msg.get('cc'),
481             'user_id': False,
482         })
483         if  msg.get('priority'):
484             custom_values['priority'] =  msg.get('priority')
485         custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from'), context=context))
486
487         res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
488         self.convert_to_bug(cr, uid, [res_id], context=context)
489         return res_id
490
491     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
492         """ Overrides mail_thread message_update that is called by the mailgateway
493             through message_process.
494             This method updates the document according to the email.
495         """
496         if isinstance(ids, (str, int, long)):
497             ids = [ids]
498         if update_vals is None: update_vals = {}
499
500         # Update doc values according to the message
501         update_vals['description'] = msg.get('body_text', '')
502         if msg.get('priority'):
503             update_vals['priority'] = msg.get('priority')
504         # Parse 'body_text' to find values to update
505         maps = {
506             'cost': 'planned_cost',
507             'revenue': 'planned_revenue',
508             'probability': 'probability',
509         }
510         for line in msg.get('body_text', '').split('\n'):
511             line = line.strip()
512             res = tools.misc.command_re.match(line)
513             if res and maps.get(res.group(1).lower(), False):
514                 key = maps.get(res.group(1).lower())
515                 update_vals[key] = res.group(2).lower()
516
517         return super(project_issue, self).message_update(cr, uid, ids, update_vals=update_vals, context=context)
518     
519     # -------------------------------------------------------
520     # OpenChatter methods and notifications
521     # -------------------------------------------------------
522     
523     def message_get_subscribers(self, cr, uid, ids, context=None):
524         """ Override to add responsible user. """
525         user_ids = super(project_issue, self).message_get_subscribers(cr, uid, ids, context=context)
526         for obj in self.browse(cr, uid, ids, context=context):
527             if obj.user_id and not obj.user_id.id in user_ids:
528                 user_ids.append(obj.user_id.id)
529         return user_ids
530
531     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
532         """ Override of the (void) default notification method. """
533         stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
534         return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
535
536     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
537         """ Override of default prefix for notifications. """
538         return 'Project issue'
539
540     def convert_to_task_send_note(self, cr, uid, ids, context=None):
541         message = _("Project issue has been <b>converted</b> in to task.")
542         return self.message_append_note(cr, uid, ids, body=message, context=context)
543
544     def create_send_note(self, cr, uid, ids, context=None):
545         message = _("Project issue has been <b>created</b>.")
546         return self.message_append_note(cr, uid, ids, body=message, context=context)
547
548     def case_escalate_send_note(self, cr, uid, ids, context=None):
549         for obj in self.browse(cr, uid, ids, context=context):
550             if obj.project_id:
551                 message = _("has been <b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
552                 obj.message_append_note(body=message, context=context)
553             else:
554                 message = _("has been <b>escalated</b>.")
555                 obj.message_append_note(body=message, context=context)
556         return True
557
558 project_issue()
559
560 class project(osv.osv):
561     _inherit = "project.project"
562
563     def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
564         res = dict.fromkeys(ids, 0)
565         issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
566         for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
567             res[issue.project_id.id] += 1
568         return res
569
570     _columns = {
571         '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)]}),
572         'reply_to' : fields.char('Reply-To Email Address', size=256),
573         'issue_count': fields.function(_issue_count, type='integer'),
574     }
575
576     def _check_escalation(self, cr, uid, ids, context=None):
577         project_obj = self.browse(cr, uid, ids[0], context=context)
578         if project_obj.project_escalation_id:
579             if project_obj.project_escalation_id.id == project_obj.id:
580                 return False
581         return True
582
583     _constraints = [
584         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
585     ]
586 project()
587
588 class account_analytic_account(osv.osv):
589     _inherit = 'account.analytic.account'
590     _description = 'Analytic Account'
591     
592     _columns = {
593         'use_issues' : fields.boolean('Issues Tracking', help="Check this field if this project manages issues"),
594     }
595     
596     def _trigger_project_creation(self, cr, uid, vals, context=None):
597         res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
598         return res or vals.get('use_issues')
599
600 account_analytic_account()
601
602 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: