[MERGE]: Merged lp:~openerp-dev/openobject-addons/trunk-auto_follow-atp-auto_follow_p...
[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 = ['mail.thread', 'ir.needaction_mixin']
50
51     def _get_default_project_id(self, cr, uid, context=None):
52         """ Gives default project by checking if present in the context """
53         return self._resolve_project_id_from_context(cr, uid, context=context)
54
55     def _get_default_stage_id(self, cr, uid, context=None):
56         """ Gives default stage_id """
57         project_id = self._get_default_project_id(cr, uid, context=context)
58         return self.stage_find(cr, uid, [], project_id, [('state', '=', 'draft')], context=context)
59
60     def _resolve_project_id_from_context(self, cr, uid, context=None):
61         """ Returns ID of project based on the value of 'default_project_id'
62             context key, or None if it cannot be resolved to a single
63             project.
64         """
65         if context is None:
66             context = {}
67         if type(context.get('default_project_id')) in (int, long):
68             return context.get('default_project_id')
69         if isinstance(context.get('default_project_id'), basestring):
70             project_name = context['default_project_id']
71             project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
72             if len(project_ids) == 1:
73                 return int(project_ids[0][0])
74         return None
75
76     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
77         access_rights_uid = access_rights_uid or uid
78         stage_obj = self.pool.get('project.task.type')
79         order = stage_obj._order
80         # lame hack to allow reverting search, should just work in the trivial case
81         if read_group_order == 'stage_id desc':
82             order = "%s desc" % order
83         # retrieve section_id from the context and write the domain
84         # - ('id', 'in', 'ids'): add columns that should be present
85         # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
86         # - OR ('project_ids', 'in', project_id), ('fold', '=', False) if project_id: add project columns that are not folded
87         search_domain = []
88         project_id = self._resolve_project_id_from_context(cr, uid, context=context)
89         if project_id:
90             search_domain += ['|', ('project_ids', '=', project_id)]
91         search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
92         # perform search
93         stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
94         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
95         # restore order of the search
96         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
97
98         fold = {}
99         for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
100             fold[stage.id] = stage.fold or False
101         return result, fold
102
103     def _compute_day(self, cr, uid, ids, fields, args, context=None):
104         """
105         @param cr: the current row, from the database cursor,
106         @param uid: the current user’s ID for security checks,
107         @param ids: List of Openday’s IDs
108         @return: difference between current date and log date
109         @param context: A standard dictionary for contextual values
110         """
111         cal_obj = self.pool.get('resource.calendar')
112         res_obj = self.pool.get('resource.resource')
113
114         res = {}
115         for issue in self.browse(cr, uid, ids, context=context):
116             res[issue.id] = {}
117             for field in fields:
118                 duration = 0
119                 ans = False
120                 hours = 0
121
122                 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
123                 if field in ['working_hours_open','day_open']:
124                     if issue.date_open:
125                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
126                         ans = date_open - date_create
127                         date_until = issue.date_open
128                         #Calculating no. of working hours to open the issue
129                         if issue.project_id.resource_calendar_id:
130                             hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
131                                                            date_create,
132                                                            date_open)
133                 elif field in ['working_hours_close','day_close']:
134                     if issue.date_closed:
135                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
136                         date_until = issue.date_closed
137                         ans = date_close - date_create
138                         #Calculating no. of working hours to close the issue
139                         if issue.project_id.resource_calendar_id:
140                             hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
141                                date_create,
142                                date_close)
143                 elif field in ['days_since_creation']:
144                     if issue.create_date:
145                         days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
146                         res[issue.id][field] = days_since_creation.days
147                     continue
148
149                 elif field in ['inactivity_days']:
150                     res[issue.id][field] = 0
151                     if issue.date_action_last:
152                         inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
153                         res[issue.id][field] = inactive_days.days
154                     continue
155                 if ans:
156                     resource_id = False
157                     if issue.user_id:
158                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
159                         if resource_ids and len(resource_ids):
160                             resource_id = resource_ids[0]
161                     duration = float(ans.days)
162                     if issue.project_id and issue.project_id.resource_calendar_id:
163                         duration = float(ans.days) * 24
164
165                         new_dates = cal_obj.interval_min_get(cr, uid,
166                                                              issue.project_id.resource_calendar_id.id,
167                                                              date_create,
168                                                              duration, resource=resource_id)
169                         no_days = []
170                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
171                         for in_time, out_time in new_dates:
172                             if in_time.date not in no_days:
173                                 no_days.append(in_time.date)
174                             if out_time > date_until:
175                                 break
176                         duration = len(no_days)
177
178                 if field in ['working_hours_open','working_hours_close']:
179                     res[issue.id][field] = hours
180                 else:
181                     res[issue.id][field] = abs(float(duration))
182
183         return res
184
185     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
186         task_pool = self.pool.get('project.task')
187         res = {}
188         for issue in self.browse(cr, uid, ids, context=context):
189             progress = 0.0
190             if issue.task_id:
191                 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
192             res[issue.id] = {'progress' : progress}
193         return res
194
195     def on_change_project(self, cr, uid, ids, project_id, context=None):
196         return {}
197
198     def _get_issue_task(self, cr, uid, ids, context=None):
199         issues = []
200         issue_pool = self.pool.get('project.issue')
201         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
202             issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
203         return issues
204
205     def _get_issue_work(self, cr, uid, ids, context=None):
206         issues = []
207         issue_pool = self.pool.get('project.issue')
208         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
209             if work.task_id:
210                 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
211         return issues
212
213     _columns = {
214         'id': fields.integer('ID', readonly=True),
215         'name': fields.char('Issue', size=128, required=True),
216         'active': fields.boolean('Active', required=False),
217         'create_date': fields.datetime('Creation Date', readonly=True,select=True),
218         'write_date': fields.datetime('Update Date', readonly=True),
219         'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
220                                                multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
221         'date_deadline': fields.date('Deadline'),
222         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
223                         select=True, help='Sales team to which Case belongs to.\
224                              Define Responsible user and Email account for mail gateway.'),
225         'partner_id': fields.many2one('res.partner', 'Contact', select=1),
226         'company_id': fields.many2one('res.company', 'Company'),
227         'description': fields.text('Description'),
228         'state': fields.related('stage_id', 'state', type="selection", store=True,
229                 selection=_ISSUE_STATE, string="State", readonly=True,
230                 help='The state is set to \'Draft\', when a case is created.\
231                       If the case is in progress the state is set to \'Open\'.\
232                       When the case is over, the state is set to \'Done\'.\
233                       If the case needs to be reviewed then the state is \
234                       set to \'Pending\'.'),
235         'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
236         '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"),
237         'date_open': fields.datetime('Opened', readonly=True,select=True),
238         # Project Issue fields
239         'date_closed': fields.datetime('Closed', readonly=True,select=True),
240         'date': fields.datetime('Date'),
241         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
242         'categ_ids': fields.many2many('project.category', string='Categories'),
243         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
244         'version_id': fields.many2one('project.issue.version', 'Version'),
245         'stage_id': fields.many2one ('project.task.type', 'Stage',
246                         domain="['&', ('fold', '=', False), '|', ('project_ids', '=', project_id), ('case_default', '=', True)]"),
247         'project_id':fields.many2one('project.project', 'Project'),
248         'duration': fields.float('Duration'),
249         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
250         'day_open': fields.function(_compute_day, string='Days to Open', \
251                                 multi='compute_day', type="float", store=True),
252         'day_close': fields.function(_compute_day, string='Days to Close', \
253                                 multi='compute_day', type="float", store=True),
254         'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
255         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
256                                 multi='compute_day', type="float", store=True),
257         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
258                                 multi='compute_day', type="float", store=True),
259         'inactivity_days': fields.function(_compute_day, string='Days since last action', \
260                                 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
261         'color': fields.integer('Color Index'),
262         'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
263         'date_action_last': fields.datetime('Last Action', readonly=1),
264         'date_action_next': fields.datetime('Next Action', readonly=1),
265         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
266             store = {
267                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
268                 'project.task': (_get_issue_task, ['progress'], 10),
269                 'project.task.work': (_get_issue_work, ['hours'], 10),
270             }),
271     }
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          }
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                 'state':'pending'
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['name'] = issue['name'] + _(' (copy)')
359         return super(project_issue, self).copy(cr, uid, id, default=default,
360                 context=context)
361
362     def write(self, cr, uid, ids, vals, context=None):
363         #Update last action date every time the user change the stage, the state or send a new email
364         logged_fields = ['stage_id', 'state', 'message_ids']
365         if any([field in vals for field in logged_fields]):
366             vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
367         return super(project_issue, self).write(cr, uid, ids, vals, context)
368
369     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
370         if not task_id:
371             return {'value':{}}
372         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
373         return {'value':{'user_id': task.user_id.id,}}
374
375     def case_reset(self, cr, uid, ids, context=None):
376         """Resets case as draft
377         """
378         res = super(project_issue, self).case_reset(cr, uid, ids, context)
379         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
380         return res
381
382     def project_message_followers(self, cr, uid, issue_id, context=None):
383         project_obj = self.pool.get("project.project")
384         project_id = self.browse(cr, uid, issue_id, context=context).project_id
385         followers = [follower.id for follower in project_obj.browse(cr, uid, project_id.id, context=context).message_follower_ids]
386         self.message_subscribe(cr, uid, issue_id, followers, context=context)
387         return True
388
389     def create(self, cr, uid, vals, context=None):
390         obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
391         self.project_message_followers(cr, uid, obj_id, context=context)
392         self.create_send_note(cr, uid, [obj_id], context=context)
393         return obj_id
394
395     # -------------------------------------------------------
396     # Stage management
397     # -------------------------------------------------------
398
399     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
400         """ Override of the base.stage method
401             Parameter of the stage search taken from the issue:
402             - type: stage type must be the same or 'both'
403             - section_id: if set, stages must belong to this section or
404               be a default case
405         """
406         if isinstance(cases, (int, long)):
407             cases = self.browse(cr, uid, cases, context=context)
408         # collect all section_ids
409         section_ids = []
410         if section_id:
411             section_ids.append(section_id)
412         for task in cases:
413             if task.project_id:
414                 section_ids.append(task.project_id.id)
415         # OR all section_ids and OR with case_default
416         search_domain = []
417         if section_ids:
418             search_domain += [('|')] * len(section_ids)
419             for section_id in section_ids:
420                 search_domain.append(('project_ids', '=', section_id))
421         search_domain.append(('case_default', '=', True))
422         # AND with the domain in parameter
423         search_domain += list(domain)
424         # perform search, return the first found
425         stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
426         if stage_ids:
427             return stage_ids[0]
428         return False
429
430     def case_cancel(self, cr, uid, ids, context=None):
431         """ Cancels case """
432         self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
433         self.case_cancel_send_note(cr, uid, ids, context=context)
434         return True
435
436     def case_escalate(self, cr, uid, ids, context=None):
437         cases = self.browse(cr, uid, ids)
438         for case in cases:
439             data = {}
440             if case.project_id.project_escalation_id:
441                 data['project_id'] = case.project_id.project_escalation_id.id
442                 if case.project_id.project_escalation_id.user_id:
443                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
444                 if case.task_id:
445                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
446             else:
447                 raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
448             self.case_set(cr, uid, ids, 'draft', data, context=context)
449             self.case_escalate_send_note(cr, uid, [case.id], context=context)
450         return True
451
452     # -------------------------------------------------------
453     # Mail gateway
454     # -------------------------------------------------------
455
456     def message_new(self, cr, uid, msg, custom_values=None, context=None):
457         """ Overrides mail_thread message_new that is called by the mailgateway
458             through message_process.
459             This override updates the document according to the email.
460         """
461         if custom_values is None: custom_values = {}
462         if context is None: context = {}
463         context['state_to'] = 'draft'
464
465         custom_values.update({
466             'name':  msg.get('subject') or _("No Subject"),
467             'description': msg.get('body'),
468             'email_from': msg.get('from'),
469             'email_cc': msg.get('cc'),
470             'user_id': False,
471         })
472         if  msg.get('priority'):
473             custom_values['priority'] =  msg.get('priority')
474
475         res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
476         # self.convert_to_bug(cr, uid, [res_id], context=context)
477         return res_id
478
479     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
480         """ Overrides mail_thread message_update that is called by the mailgateway
481             through message_process.
482             This method updates the document according to the email.
483         """
484         if isinstance(ids, (str, int, long)):
485             ids = [ids]
486         if update_vals is None: update_vals = {}
487
488         # Update doc values according to the message
489         update_vals['description'] = msg.get('body', '')
490         if msg.get('priority'):
491             update_vals['priority'] = msg.get('priority')
492         # Parse 'body' to find values to update
493         maps = {
494             'cost': 'planned_cost',
495             'revenue': 'planned_revenue',
496             'probability': 'probability',
497         }
498         for line in msg.get('body', '').split('\n'):
499             line = line.strip()
500             res = tools.misc.command_re.match(line)
501             if res and maps.get(res.group(1).lower(), False):
502                 key = maps.get(res.group(1).lower())
503                 update_vals[key] = res.group(2).lower()
504
505         return super(project_issue, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
506
507     # -------------------------------------------------------
508     # OpenChatter methods and notifications
509     # -------------------------------------------------------
510
511     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
512         """ Override of the (void) default notification method. """
513         stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
514         return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
515
516     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
517         """ Override of default prefix for notifications. """
518         return 'Project issue'
519
520     def convert_to_task_send_note(self, cr, uid, ids, context=None):
521         message = _("Project issue <b>converted</b> to task.")
522         return self.message_post(cr, uid, ids, body=message, context=context)
523
524     def create_send_note(self, cr, uid, ids, context=None):
525         message = _("Project issue <b>created</b>.")
526         return self.message_post(cr, uid, ids, body=message, context=context)
527
528     def case_escalate_send_note(self, cr, uid, ids, context=None):
529         for obj in self.browse(cr, uid, ids, context=context):
530             if obj.project_id:
531                 message = _("<b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
532                 obj.message_post(body=message)
533             else:
534                 message = _("<b>escalated</b>.")
535                 obj.message_post(body=message)
536         return True
537
538 project_issue()
539
540 class project(osv.osv):
541     _inherit = "project.project"
542
543     def _get_alias_models(self, cr, uid, context=None):
544         return [('project.task', "Tasks"), ("project.issue", "Issues")]
545         
546     def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
547         res = dict.fromkeys(ids, 0)
548         issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
549         for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
550             res[issue.project_id.id] += 1
551         return res
552
553     _columns = {
554         '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)]}),
555         'issue_count': fields.function(_issue_count, type='integer'),
556     }
557
558     def _check_escalation(self, cr, uid, ids, context=None):
559         project_obj = self.browse(cr, uid, ids[0], context=context)
560         if project_obj.project_escalation_id:
561             if project_obj.project_escalation_id.id == project_obj.id:
562                 return False
563         return True
564
565     _constraints = [
566         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
567     ]
568     
569 project()
570
571 class account_analytic_account(osv.osv):
572     _inherit = 'account.analytic.account'
573     _description = 'Analytic Account'
574
575     _columns = {
576         'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
577     }
578
579     def on_change_template(self, cr, uid, ids, template_id, context=None):
580         res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
581         if template_id and 'value' in res:
582             template = self.browse(cr, uid, template_id, context=context)
583             res['value']['use_issues'] = template.use_issues
584         return res
585
586     def _trigger_project_creation(self, cr, uid, vals, context=None):
587         if context is None: context = {}
588         res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
589         return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
590
591 account_analytic_account()
592
593 class project_project(osv.osv):
594     _inherit = 'project.project'
595     _defaults = {
596         'use_issues': True
597     }
598
599 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: