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