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