[FIX] project_issue: fixed subtype implementation, about followers of project automat...
[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         # task followers are project followers, with matching subtypes
364         task_record = self.browse(cr, uid, task_id, context=context)
365         subtype_obj = self.pool.get('mail.message.subtype')
366         follower_obj = self.pool.get('mail.followers')
367         if task_record.project_id:
368             # create mapping
369             task_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('res_model', '=', self._name)], context=context)
370             task_subtypes = subtype_obj.browse(cr, uid, task_subtype_ids, context=context)
371             # fetch subscriptions
372             follower_ids = follower_obj.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', task_record.project_id.id)], context=context)
373             # copy followers
374             for follower in follower_obj.browse(cr, uid, follower_ids, context=context):
375                 if not follower.subtype_ids:
376                     continue
377                 project_subtype_names = [project_subtype.name for project_subtype in follower.subtype_ids]
378                 task_subtype_ids = [task_subtype.id for task_subtype in task_subtypes if task_subtype.name in project_subtype_names]
379                 self.message_subscribe(cr, uid, [task_id], [follower.partner_id.id],
380                     subtype_ids=task_subtype_ids, 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
388         # subscribe new project followers to the issue
389         if vals.get('project_id'):
390             for id in ids:
391                 self._subscribe_project_followers_to_issue(cr, uid, id, context=context)
392
393         return super(project_issue, self).write(cr, uid, ids, vals, context)
394
395     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
396         if not task_id:
397             return {'value': {}}
398         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
399         return {'value': {'user_id': task.user_id.id, }}
400
401     def case_reset(self, cr, uid, ids, context=None):
402         """Resets case as draft
403         """
404         res = super(project_issue, self).case_reset(cr, uid, ids, context)
405         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
406         return res
407
408     def create(self, cr, uid, vals, context=None):
409         obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
410
411         # subscribe project follower to the issue
412         self._subscribe_project_followers_to_issue(cr, uid, obj_id, context=context)
413         self.create_send_note(cr, uid, [obj_id], context=context)
414
415         return obj_id
416
417     # -------------------------------------------------------
418     # Stage management
419     # -------------------------------------------------------
420
421     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
422         """ Override of the base.stage method
423             Parameter of the stage search taken from the issue:
424             - type: stage type must be the same or 'both'
425             - section_id: if set, stages must belong to this section or
426               be a default case
427         """
428         if isinstance(cases, (int, long)):
429             cases = self.browse(cr, uid, cases, context=context)
430         # collect all section_ids
431         section_ids = []
432         if section_id:
433             section_ids.append(section_id)
434         for task in cases:
435             if task.project_id:
436                 section_ids.append(task.project_id.id)
437         # OR all section_ids and OR with case_default
438         search_domain = []
439         if section_ids:
440             search_domain += [('|')] * len(section_ids)
441             for section_id in section_ids:
442                 search_domain.append(('project_ids', '=', section_id))
443         search_domain.append(('case_default', '=', True))
444         # AND with the domain in parameter
445         search_domain += list(domain)
446         # perform search, return the first found
447         stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
448         if stage_ids:
449             return stage_ids[0]
450         return False
451
452     def case_cancel(self, cr, uid, ids, context=None):
453         """ Cancels case """
454         self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
455         self.case_cancel_send_note(cr, uid, ids, context=context)
456         return True
457
458     def case_escalate(self, cr, uid, ids, context=None):
459         cases = self.browse(cr, uid, ids)
460         for case in cases:
461             data = {}
462             if case.project_id.project_escalation_id:
463                 data['project_id'] = case.project_id.project_escalation_id.id
464                 if case.project_id.project_escalation_id.user_id:
465                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
466                 if case.task_id:
467                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
468             else:
469                 raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
470             self.case_set(cr, uid, ids, 'draft', data, context=context)
471             self.case_escalate_send_note(cr, uid, [case.id], context=context)
472         return True
473
474     # -------------------------------------------------------
475     # Mail gateway
476     # -------------------------------------------------------
477
478     def message_new(self, cr, uid, msg, custom_values=None, context=None):
479         """ Overrides mail_thread message_new that is called by the mailgateway
480             through message_process.
481             This override updates the document according to the email.
482         """
483         if custom_values is None: custom_values = {}
484         if context is None: context = {}
485         context['state_to'] = 'draft'
486
487         desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
488
489         custom_values.update({
490             'name':  msg.get('subject') or _("No Subject"),
491             'description': desc,
492             'email_from': msg.get('from'),
493             'email_cc': msg.get('cc'),
494             'user_id': False,
495         })
496         if  msg.get('priority'):
497             custom_values['priority'] =  msg.get('priority')
498
499         res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
500         # self.convert_to_bug(cr, uid, [res_id], context=context)
501         return res_id
502
503     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
504         """ Overrides mail_thread message_update that is called by the mailgateway
505             through message_process.
506             This method updates the document according to the email.
507         """
508         if isinstance(ids, (str, int, long)):
509             ids = [ids]
510         if update_vals is None: update_vals = {}
511
512         # Update doc values according to the message
513         if msg.get('priority'):
514             update_vals['priority'] = msg.get('priority')
515         # Parse 'body' to find values to update
516         maps = {
517             'cost': 'planned_cost',
518             'revenue': 'planned_revenue',
519             'probability': 'probability',
520         }
521         for line in msg.get('body', '').split('\n'):
522             line = line.strip()
523             res = tools.misc.command_re.match(line)
524             if res and maps.get(res.group(1).lower(), False):
525                 key = maps.get(res.group(1).lower())
526                 update_vals[key] = res.group(2).lower()
527
528         return super(project_issue, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
529
530     # -------------------------------------------------------
531     # OpenChatter methods and notifications
532     # -------------------------------------------------------
533
534     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
535         """ Override of the (void) default notification method. """
536         stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
537         return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), subtype="mt_issue_new", context=context)
538
539     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
540         """ Override of default prefix for notifications. """
541         return 'Project issue'
542
543     def convert_to_task_send_note(self, cr, uid, ids, context=None):
544         message = _("Project issue <b>converted</b> to task.")
545         return self.message_post(cr, uid, ids, body=message, context=context)
546
547     def create_send_note(self, cr, uid, ids, context=None):
548         message = _("Project issue <b>created</b>.")
549         return self.message_post(cr, uid, ids, body=message, subtype="mt_issue_new", context=context)
550
551     def case_escalate_send_note(self, cr, uid, ids, context=None):
552         for obj in self.browse(cr, uid, ids, context=context):
553             if obj.project_id:
554                 message = _("<b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
555                 obj.message_post(body=message)
556             else:
557                 message = _("<b>escalated</b>.")
558                 obj.message_post(body=message)
559         return True
560
561 project_issue()
562
563 class project(osv.osv):
564     _inherit = "project.project"
565
566     def _get_alias_models(self, cr, uid, context=None):
567         return [('project.task', "Tasks"), ("project.issue", "Issues")]
568
569     def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
570         res = dict.fromkeys(ids, 0)
571         issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
572         for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
573             res[issue.project_id.id] += 1
574         return res
575
576     _columns = {
577         '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)]}),
578         'issue_count': fields.function(_issue_count, type='integer'),
579     }
580
581     def _check_escalation(self, cr, uid, ids, context=None):
582         project_obj = self.browse(cr, uid, ids[0], context=context)
583         if project_obj.project_escalation_id:
584             if project_obj.project_escalation_id.id == project_obj.id:
585                 return False
586         return True
587
588     _constraints = [
589         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
590     ]
591
592 project()
593
594 class account_analytic_account(osv.osv):
595     _inherit = 'account.analytic.account'
596     _description = 'Analytic Account'
597
598     _columns = {
599         'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
600     }
601
602     def on_change_template(self, cr, uid, ids, template_id, context=None):
603         res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
604         if template_id and 'value' in res:
605             template = self.browse(cr, uid, template_id, context=context)
606             res['value']['use_issues'] = template.use_issues
607         return res
608
609     def _trigger_project_creation(self, cr, uid, vals, context=None):
610         if context is None: context = {}
611         res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
612         return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
613
614 account_analytic_account()
615
616 class project_project(osv.osv):
617     _inherit = 'project.project'
618     _defaults = {
619         'use_issues': True
620     }
621
622 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: