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