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