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