[IMP]:changed stage while click on the stages
[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 from 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 class project_issue(base_stage, osv.osv):
47     _name = "project.issue"
48     _description = "Project Issue"
49     _order = "priority, create_date desc"
50     _inherit = ['mail.thread', 'ir.needaction_mixin']
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)]
92         search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
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
99         fold = {}
100         for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
101             fold[stage.id] = stage.fold or False
102         return result, fold
103
104     def _compute_day(self, cr, uid, ids, fields, args, context=None):
105         """
106         @param cr: the current row, from the database cursor,
107         @param uid: the current user’s ID for security checks,
108         @param ids: List of Openday’s IDs
109         @return: difference between current date and log date
110         @param context: A standard dictionary for contextual values
111         """
112         cal_obj = self.pool.get('resource.calendar')
113         res_obj = self.pool.get('resource.resource')
114
115         res = {}
116         for issue in self.browse(cr, uid, ids, context=context):
117             res[issue.id] = {}
118             for field in fields:
119                 duration = 0
120                 ans = False
121                 hours = 0
122
123                 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
124                 if field in ['working_hours_open','day_open']:
125                     if issue.date_open:
126                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
127                         ans = date_open - date_create
128                         date_until = issue.date_open
129                         #Calculating no. of working hours to open the issue
130                         if issue.project_id.resource_calendar_id:
131                             hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
132                                                            date_create,
133                                                            date_open)
134                 elif field in ['working_hours_close','day_close']:
135                     if issue.date_closed:
136                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
137                         date_until = issue.date_closed
138                         ans = date_close - date_create
139                         #Calculating no. of working hours to close the issue
140                         if issue.project_id.resource_calendar_id:
141                             hours = cal_obj.interval_hours_get(cr, uid, issue.project_id.resource_calendar_id.id,
142                                date_create,
143                                date_close)
144                 elif field in ['days_since_creation']:
145                     if issue.create_date:
146                         days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
147                         res[issue.id][field] = days_since_creation.days
148                     continue
149
150                 elif field in ['inactivity_days']:
151                     res[issue.id][field] = 0
152                     if issue.date_action_last:
153                         inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S')
154                         res[issue.id][field] = inactive_days.days
155                     continue
156                 if ans:
157                     resource_id = False
158                     if issue.user_id:
159                         resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)])
160                         if resource_ids and len(resource_ids):
161                             resource_id = resource_ids[0]
162                     duration = float(ans.days)
163                     if issue.project_id and issue.project_id.resource_calendar_id:
164                         duration = float(ans.days) * 24
165
166                         new_dates = cal_obj.interval_min_get(cr, uid,
167                                                              issue.project_id.resource_calendar_id.id,
168                                                              date_create,
169                                                              duration, resource=resource_id)
170                         no_days = []
171                         date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S')
172                         for in_time, out_time in new_dates:
173                             if in_time.date not in no_days:
174                                 no_days.append(in_time.date)
175                             if out_time > date_until:
176                                 break
177                         duration = len(no_days)
178
179                 if field in ['working_hours_open','working_hours_close']:
180                     res[issue.id][field] = hours
181                 else:
182                     res[issue.id][field] = abs(float(duration))
183
184         return res
185
186     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
187         task_pool = self.pool.get('project.task')
188         res = {}
189         for issue in self.browse(cr, uid, ids, context=context):
190             progress = 0.0
191             if issue.task_id:
192                 progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
193             res[issue.id] = {'progress' : progress}
194         return res
195
196     def on_change_project(self, cr, uid, ids, project_id, context=None):
197         return {}
198
199     def _get_issue_task(self, cr, uid, ids, context=None):
200         issues = []
201         issue_pool = self.pool.get('project.issue')
202         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
203             issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
204         return issues
205
206     def _get_issue_work(self, cr, uid, ids, context=None):
207         issues = []
208         issue_pool = self.pool.get('project.issue')
209         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
210             if work.task_id:
211                 issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)])
212         return issues
213
214     _columns = {
215         'id': fields.integer('ID', readonly=True),
216         'name': fields.char('Issue', size=128, required=True),
217         'active': fields.boolean('Active', required=False),
218         'create_date': fields.datetime('Creation Date', readonly=True,select=True),
219         'write_date': fields.datetime('Update Date', readonly=True),
220         'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
221                                                multi='compute_day', type="integer", help="Difference in days between creation date and current date"),
222         'date_deadline': fields.date('Deadline'),
223         'section_id': fields.many2one('crm.case.section', 'Sales Team', \
224                         select=True, help='Sales team to which Case belongs to.\
225                              Define Responsible user and Email account for mail gateway.'),
226         'partner_id': fields.many2one('res.partner', 'Contact', select=1),
227         'company_id': fields.many2one('res.company', 'Company'),
228         'description': fields.text('Description'),
229         'state': fields.related('stage_id', 'state', type="selection", store=True,
230                 selection=_ISSUE_STATE, string="Status", readonly=True,
231                 help='The status is set to \'Draft\', when a case is created.\
232                       If the case is in progress the status is set to \'Open\'.\
233                       When the case is over, the status is set to \'Done\'.\
234                       If the case needs to be reviewed then the status is \
235                       set to \'Pending\'.'),
236         'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
237         '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"),
238         'date_open': fields.datetime('Opened', readonly=True,select=True),
239         # Project Issue fields
240         'date_closed': fields.datetime('Closed', readonly=True,select=True),
241         'date': fields.datetime('Date'),
242         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
243         'categ_ids': fields.many2many('project.category', string='Tags'),
244         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
245         'version_id': fields.many2one('project.issue.version', 'Version'),
246         'stage_id': fields.many2one ('project.task.type', 'Stage',
247                         domain="['&', ('fold', '=', False), '|', ('project_ids', '=', project_id), ('case_default', '=', True)]"),
248         'project_id':fields.many2one('project.project', 'Project'),
249         'duration': fields.float('Duration'),
250         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
251         'day_open': fields.function(_compute_day, string='Days to Open', \
252                                 multi='compute_day', type="float", store=True),
253         'day_close': fields.function(_compute_day, string='Days to Close', \
254                                 multi='compute_day', type="float", store=True),
255         'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
256         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
257                                 multi='compute_day', type="float", store=True),
258         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
259                                 multi='compute_day', type="float", store=True),
260         'inactivity_days': fields.function(_compute_day, string='Days since last action', \
261                                 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
262         'color': fields.integer('Color Index'),
263         'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
264         'date_action_last': fields.datetime('Last Action', readonly=1),
265         'date_action_next': fields.datetime('Next Action', readonly=1),
266         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
267             store = {
268                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
269                 'project.task': (_get_issue_task, ['progress'], 10),
270                 'project.task.work': (_get_issue_work, ['hours'], 10),
271             }),
272     }
273
274     _defaults = {
275         'active': 1,
276         'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
277         'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
278         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
279         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
280         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
281         'priority': crm.AVAILABLE_PRIORITIES[2][0],
282     }
283
284     _group_by_full = {
285         'stage_id': _read_group_stage_ids
286     }
287
288     def set_priority(self, cr, uid, ids, priority, *args):
289         """Set lead priority
290         """
291         return self.write(cr, uid, ids, {'priority' : priority})
292
293     def set_high_priority(self, cr, uid, ids, *args):
294         """Set lead priority to high
295         """
296         return self.set_priority(cr, uid, ids, '1')
297
298     def set_normal_priority(self, cr, uid, ids, *args):
299         """Set lead priority to normal
300         """
301         return self.set_priority(cr, uid, ids, '3')
302
303     def convert_issue_task(self, cr, uid, ids, context=None):
304         if context is None:
305             context = {}
306
307         case_obj = self.pool.get('project.issue')
308         data_obj = self.pool.get('ir.model.data')
309         task_obj = self.pool.get('project.task')
310
311         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
312         res = data_obj.read(cr, uid, result, ['res_id'])
313         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
314         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
315         if id2:
316             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
317         if id3:
318             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
319
320         for bug in case_obj.browse(cr, uid, ids, context=context):
321             new_task_id = task_obj.create(cr, uid, {
322                 'name': bug.name,
323                 'partner_id': bug.partner_id.id,
324                 'description':bug.description,
325                 'date_deadline': bug.date,
326                 'project_id': bug.project_id.id,
327                 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
328                 'priority': str(int(bug.priority) - 1),
329                 'user_id': bug.user_id.id,
330                 'planned_hours': 0.0,
331             })
332             vals = {
333                 'task_id': new_task_id,
334                 'stage_id': self.stage_find(cr, uid, [bug], bug.project_id.id, [('state', '=', 'pending')], context=context),
335             }
336             self.convert_to_task_send_note(cr, uid, [bug.id], context=context)
337             case_obj.write(cr, uid, [bug.id], vals, context=context)
338             self.case_pending_send_note(cr, uid, [bug.id], context=context)
339
340         return  {
341             'name': _('Tasks'),
342             'view_type': 'form',
343             'view_mode': 'form,tree',
344             'res_model': 'project.task',
345             'res_id': int(new_task_id),
346             'view_id': False,
347             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
348             'type': 'ir.actions.act_window',
349             'search_view_id': res['res_id'],
350             'nodestroy': True
351         }
352
353     def copy(self, cr, uid, id, default=None, context=None):
354         issue = self.read(cr, uid, id, ['name'], context=context)
355         if not default:
356             default = {}
357         default = default.copy()
358         default.update(name=_('%s (copy)') % (issue['name']))
359         return super(project_issue, self).copy(cr, uid, id, default=default,
360                 context=context)
361
362     def _subscribe_project_followers_to_issue(self, cr, uid, task_id, context=None):
363         """ TDE note: not the best way to do this, we could override _get_followers
364             of issue, and perform a better mapping of subtypes than a mapping
365             based on names.
366             However we will keep this implementation, maybe to be refactored
367             in 7.1 of future versions. """
368         # task followers are project followers, with matching subtypes
369         task_record = self.browse(cr, uid, task_id, context=context)
370         subtype_obj = self.pool.get('mail.message.subtype')
371         follower_obj = self.pool.get('mail.followers')
372         if task_record.project_id:
373             # create mapping
374             task_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('res_model', '=', self._name)], context=context)
375             task_subtypes = subtype_obj.browse(cr, uid, task_subtype_ids, context=context)
376             # fetch subscriptions
377             follower_ids = follower_obj.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', task_record.project_id.id)], context=context)
378             # copy followers
379             for follower in follower_obj.browse(cr, uid, follower_ids, context=context):
380                 if not follower.subtype_ids:
381                     continue
382                 project_subtype_names = [project_subtype.name for project_subtype in follower.subtype_ids]
383                 task_subtype_ids = [task_subtype.id for task_subtype in task_subtypes if task_subtype.name in project_subtype_names]
384                 self.message_subscribe(cr, uid, [task_id], [follower.partner_id.id],
385                     subtype_ids=task_subtype_ids, context=context)
386
387     def write(self, cr, uid, ids, vals, context=None):
388         #Update last action date every time the user change the stage, the state or send a new email
389         logged_fields = ['stage_id', 'state', 'message_ids']
390         if any([field in vals for field in logged_fields]):
391             vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
392
393         # subscribe new project followers to the issue
394         if vals.get('project_id'):
395             for id in ids:
396                 self._subscribe_project_followers_to_issue(cr, uid, id, context=context)
397         if vals.get('stage_id'):
398             self.stage_set_send_note(cr, uid, ids,vals.get('stage_id'), context=context)
399         return super(project_issue, self).write(cr, uid, ids, vals, context)
400     
401     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
402         if not task_id:
403             return {'value': {}}
404         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
405         return {'value': {'user_id': task.user_id.id, }}
406
407     def case_reset(self, cr, uid, ids, context=None):
408         """Resets case as draft
409         """
410         res = super(project_issue, self).case_reset(cr, uid, ids, context)
411         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
412         return res
413
414     def create(self, cr, uid, vals, context=None):
415         obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
416
417         # subscribe project follower to the issue
418         self._subscribe_project_followers_to_issue(cr, uid, obj_id, context=context)
419         self.create_send_note(cr, uid, [obj_id], context=context)
420
421         return obj_id
422
423     # -------------------------------------------------------
424     # Stage management
425     # -------------------------------------------------------
426
427     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
428         """ Override of the base.stage method
429             Parameter of the stage search taken from the issue:
430             - type: stage type must be the same or 'both'
431             - section_id: if set, stages must belong to this section or
432               be a default case
433         """
434         if isinstance(cases, (int, long)):
435             cases = self.browse(cr, uid, cases, context=context)
436         # collect all section_ids
437         section_ids = []
438         if section_id:
439             section_ids.append(section_id)
440         for task in cases:
441             if task.project_id:
442                 section_ids.append(task.project_id.id)
443         # OR all section_ids and OR with case_default
444         search_domain = []
445         if section_ids:
446             search_domain += [('|')] * len(section_ids)
447             for section_id in section_ids:
448                 search_domain.append(('project_ids', '=', section_id))
449         search_domain.append(('case_default', '=', True))
450         # AND with the domain in parameter
451         search_domain += list(domain)
452         # perform search, return the first found
453         stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
454         if stage_ids:
455             return stage_ids[0]
456         return False
457
458     def case_cancel(self, cr, uid, ids, context=None):
459         """ Cancels case """
460         self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
461         return True
462
463     def case_escalate(self, cr, uid, ids, context=None):
464         cases = self.browse(cr, uid, ids)
465         for case in cases:
466             data = {}
467             if case.project_id.project_escalation_id:
468                 data['project_id'] = case.project_id.project_escalation_id.id
469                 if case.project_id.project_escalation_id.user_id:
470                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
471                 if case.task_id:
472                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
473             else:
474                 raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
475             self.case_set(cr, uid, ids, 'draft', data, context=context)
476             self.case_escalate_send_note(cr, uid, [case.id], context=context)
477         return True
478
479     # -------------------------------------------------------
480     # Mail gateway
481     # -------------------------------------------------------
482
483     def message_new(self, cr, uid, msg, custom_values=None, context=None):
484         """ Overrides mail_thread message_new that is called by the mailgateway
485             through message_process.
486             This override updates the document according to the email.
487         """
488         if custom_values is None: custom_values = {}
489         if context is None: context = {}
490         context['state_to'] = 'draft'
491
492         desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
493
494         custom_values.update({
495             'name':  msg.get('subject') or _("No Subject"),
496             'description': desc,
497             'email_from': msg.get('from'),
498             'email_cc': msg.get('cc'),
499             'user_id': False,
500         })
501         if  msg.get('priority'):
502             custom_values['priority'] =  msg.get('priority')
503
504         res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
505         # self.convert_to_bug(cr, uid, [res_id], 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.misc.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     # OpenChatter methods and notifications
537     # -------------------------------------------------------
538     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
539         """ Override of the (void) default notification method. """
540         stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
541         return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), subtype="mt_issue_new", context=context)
542     
543     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
544         """ Override of default prefix for notifications. """
545         return 'Project issue'
546
547     def convert_to_task_send_note(self, cr, uid, ids, context=None):
548         message = _("Project issue <b>converted</b> to task.")
549         return self.message_post(cr, uid, ids, body=message, context=context)
550     
551     def create_send_note(self, cr, uid, ids, context=None):
552         message = _("Project issue <b>created</b>.")
553         return self.message_post(cr, uid, ids, body=message, subtype="mt_issue_new", context=context)
554
555     def case_escalate_send_note(self, cr, uid, ids, context=None):
556         for obj in self.browse(cr, uid, ids, context=context):
557             if obj.project_id:
558                 message = _("<b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
559                 obj.message_post(body=message)
560             else:
561                 message = _("<b>escalated</b>.")
562                 obj.message_post(body=message)
563         return True
564
565 project_issue()
566
567 class project(osv.osv):
568     _inherit = "project.project"
569
570     def _get_alias_models(self, cr, uid, context=None):
571         return [('project.task', "Tasks"), ("project.issue", "Issues")]
572
573     def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
574         res = dict.fromkeys(ids, 0)
575         issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
576         for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
577             res[issue.project_id.id] += 1
578         return res
579
580     _columns = {
581         '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)]}),
582         'issue_count': fields.function(_issue_count, type='integer'),
583     }
584
585     def _check_escalation(self, cr, uid, ids, context=None):
586         project_obj = self.browse(cr, uid, ids[0], context=context)
587         if project_obj.project_escalation_id:
588             if project_obj.project_escalation_id.id == project_obj.id:
589                 return False
590         return True
591
592     _constraints = [
593         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
594     ]
595
596 project()
597
598 class account_analytic_account(osv.osv):
599     _inherit = 'account.analytic.account'
600     _description = 'Analytic Account'
601
602     _columns = {
603         'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
604     }
605
606     def on_change_template(self, cr, uid, ids, template_id, context=None):
607         res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
608         if template_id and 'value' in res:
609             template = self.browse(cr, uid, template_id, context=context)
610             res['value']['use_issues'] = template.use_issues
611         return res
612
613     def _trigger_project_creation(self, cr, uid, vals, context=None):
614         if context is None: context = {}
615         res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
616         return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
617
618 account_analytic_account()
619
620 class project_project(osv.osv):
621     _inherit = 'project.project'
622     _defaults = {
623         'use_issues': True
624     }
625
626 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: