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