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