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