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