[MERGE] with trunk
[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)]
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('Private Note'),
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         'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
237                                          help="A Issue's kanban state indicates special situations affecting it:\n"
238                                               " * Normal is the default situation\n"
239                                               " * Blocked indicates something is preventing the progress of this issue\n"
240                                               " * Ready for next stage indicates the issue is ready to be pulled to the next stage",
241                                          readonly=True, required=False),
242         'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
243         '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"),
244         'date_open': fields.datetime('Opened', readonly=True,select=True),
245         # Project Issue fields
246         'date_closed': fields.datetime('Closed', readonly=True,select=True),
247         'date': fields.datetime('Date'),
248         'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel."),
249         'categ_ids': fields.many2many('project.category', string='Tags'),
250         'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
251         'version_id': fields.many2one('project.issue.version', 'Version'),
252         'stage_id': fields.many2one ('project.task.type', 'Stage',
253                         domain="['&', ('fold', '=', False), ('project_ids', '=', project_id)]"),
254         'project_id':fields.many2one('project.project', 'Project'),
255         'duration': fields.float('Duration'),
256         'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
257         'day_open': fields.function(_compute_day, string='Days to Open', \
258                                 multi='compute_day', type="float", store=True),
259         'day_close': fields.function(_compute_day, string='Days to Close', \
260                                 multi='compute_day', type="float", store=True),
261         'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
262         'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
263                                 multi='compute_day', type="float", store=True),
264         'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
265                                 multi='compute_day', type="float", store=True),
266         'inactivity_days': fields.function(_compute_day, string='Days since last action', \
267                                 multi='compute_day', type="integer", help="Difference in days between last action and current date"),
268         'color': fields.integer('Color Index'),
269         'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
270         'date_action_last': fields.datetime('Last Action', readonly=1),
271         'date_action_next': fields.datetime('Next Action', readonly=1),
272         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
273             store = {
274                 'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
275                 'project.task': (_get_issue_task, ['progress'], 10),
276                 'project.task.work': (_get_issue_work, ['hours'], 10),
277             }),
278     }
279
280     _defaults = {
281         'active': 1,
282         'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
283         'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
284         'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
285         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
286         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
287         'priority': crm.AVAILABLE_PRIORITIES[2][0],
288         'kanban_state': 'normal',
289     }
290
291     _group_by_full = {
292         'stage_id': _read_group_stage_ids
293     }
294
295     def set_priority(self, cr, uid, ids, priority, *args):
296         """Set lead priority
297         """
298         return self.write(cr, uid, ids, {'priority' : priority})
299
300     def set_high_priority(self, cr, uid, ids, *args):
301         """Set lead priority to high
302         """
303         return self.set_priority(cr, uid, ids, '1')
304
305     def set_normal_priority(self, cr, uid, ids, *args):
306         """Set lead priority to normal
307         """
308         return self.set_priority(cr, uid, ids, '3')
309
310     def convert_issue_task(self, cr, uid, ids, context=None):
311         if context is None:
312             context = {}
313
314         case_obj = self.pool.get('project.issue')
315         data_obj = self.pool.get('ir.model.data')
316         task_obj = self.pool.get('project.task')
317
318         result = data_obj._get_id(cr, uid, 'project', 'view_task_search_form')
319         res = data_obj.read(cr, uid, result, ['res_id'])
320         id2 = data_obj._get_id(cr, uid, 'project', 'view_task_form2')
321         id3 = data_obj._get_id(cr, uid, 'project', 'view_task_tree2')
322         if id2:
323             id2 = data_obj.browse(cr, uid, id2, context=context).res_id
324         if id3:
325             id3 = data_obj.browse(cr, uid, id3, context=context).res_id
326
327         for bug in case_obj.browse(cr, uid, ids, context=context):
328             new_task_id = task_obj.create(cr, uid, {
329                 'name': bug.name,
330                 'partner_id': bug.partner_id.id,
331                 'description':bug.description,
332                 'date_deadline': bug.date,
333                 'project_id': bug.project_id.id,
334                 # priority must be in ['0','1','2','3','4'], while bug.priority is in ['1','2','3','4','5']
335                 'priority': str(int(bug.priority) - 1),
336                 'user_id': bug.user_id.id,
337                 'planned_hours': 0.0,
338             })
339             vals = {
340                 'task_id': new_task_id,
341                 'stage_id': self.stage_find(cr, uid, [bug], bug.project_id.id, [('state', '=', 'pending')], context=context),
342             }
343             self.convert_to_task_send_note(cr, uid, [bug.id], context=context)
344             case_obj.write(cr, uid, [bug.id], vals, context=context)
345             self.case_pending_send_note(cr, uid, [bug.id], context=context)
346
347         return  {
348             'name': _('Tasks'),
349             'view_type': 'form',
350             'view_mode': 'form,tree',
351             'res_model': 'project.task',
352             'res_id': int(new_task_id),
353             'view_id': False,
354             'views': [(id2,'form'),(id3,'tree'),(False,'calendar'),(False,'graph')],
355             'type': 'ir.actions.act_window',
356             'search_view_id': res['res_id'],
357             'nodestroy': True
358         }
359
360     def copy(self, cr, uid, id, default=None, context=None):
361         issue = self.read(cr, uid, id, ['name'], context=context)
362         if not default:
363             default = {}
364         default = default.copy()
365         default.update(name=_('%s (copy)') % (issue['name']))
366         return super(project_issue, self).copy(cr, uid, id, default=default,
367                 context=context)
368
369     def _subscribe_project_followers_to_issue(self, cr, uid, task_id, context=None):
370         """ TDE note: not the best way to do this, we could override _get_followers
371             of issue, and perform a better mapping of subtypes than a mapping
372             based on names.
373             However we will keep this implementation, maybe to be refactored
374             in 7.1 of future versions. """
375         # task followers are project followers, with matching subtypes
376         task_record = self.browse(cr, uid, task_id, context=context)
377         subtype_obj = self.pool.get('mail.message.subtype')
378         follower_obj = self.pool.get('mail.followers')
379         if task_record.project_id:
380             # create mapping
381             task_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('res_model', '=', self._name)], context=context)
382             task_subtypes = subtype_obj.browse(cr, uid, task_subtype_ids, context=context)
383             # fetch subscriptions
384             follower_ids = follower_obj.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', task_record.project_id.id)], context=context)
385             # copy followers
386             for follower in follower_obj.browse(cr, uid, follower_ids, context=context):
387                 if not follower.subtype_ids:
388                     continue
389                 project_subtype_names = [project_subtype.name for project_subtype in follower.subtype_ids]
390                 task_subtype_ids = [task_subtype.id for task_subtype in task_subtypes if task_subtype.name in project_subtype_names]
391                 self.message_subscribe(cr, uid, [task_id], [follower.partner_id.id],
392                     subtype_ids=task_subtype_ids, context=context)
393
394     def write(self, cr, uid, ids, vals, context=None):
395         #Update last action date every time the user change the stage, the state or send a new email
396         logged_fields = ['stage_id', 'state', 'message_ids']
397         if any([field in vals for field in logged_fields]):
398             vals['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S')
399
400         # subscribe new project followers to the issue
401         if vals.get('project_id'):
402             for id in ids:
403                 self._subscribe_project_followers_to_issue(cr, uid, id, context=context)
404
405         return super(project_issue, self).write(cr, uid, ids, vals, context)
406     
407     def onchange_task_id(self, cr, uid, ids, task_id, context=None):
408         if not task_id:
409             return {'value': {}}
410         task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
411         return {'value': {'user_id': task.user_id.id, }}
412
413     def case_reset(self, cr, uid, ids, context=None):
414         """Resets case as draft
415         """
416         res = super(project_issue, self).case_reset(cr, uid, ids, context)
417         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
418         return res
419
420     def create(self, cr, uid, vals, context=None):
421         obj_id = super(project_issue, self).create(cr, uid, vals, context=context)
422
423         # subscribe project follower to the issue
424         self._subscribe_project_followers_to_issue(cr, uid, obj_id, context=context)
425         self.create_send_note(cr, uid, [obj_id], context=context)
426
427         return obj_id
428
429     # -------------------------------------------------------
430     # Stage management
431     # -------------------------------------------------------
432
433     def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
434         """ Override of the base.stage method
435             Parameter of the stage search taken from the issue:
436             - type: stage type must be the same or 'both'
437             - section_id: if set, stages must belong to this section or
438               be a default case
439         """
440         if isinstance(cases, (int, long)):
441             cases = self.browse(cr, uid, cases, context=context)
442         # collect all section_ids
443         section_ids = []
444         if section_id:
445             section_ids.append(section_id)
446         for task in cases:
447             if task.project_id:
448                 section_ids.append(task.project_id.id)
449         # OR all section_ids and OR with case_default
450         search_domain = []
451         if section_ids:
452             search_domain += [('|')] * (len(section_ids)-1)
453             for section_id in section_ids:
454                 search_domain.append(('project_ids', '=', section_id))
455         search_domain += list(domain)
456         # perform search, return the first found
457         stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
458         if stage_ids:
459             return stage_ids[0]
460         return False
461
462     def case_cancel(self, cr, uid, ids, context=None):
463         """ Cancels case """
464         self.case_set(cr, uid, ids, 'cancelled', {'active': True}, context=context)
465         return True
466
467     def case_escalate(self, cr, uid, ids, context=None):
468         cases = self.browse(cr, uid, ids)
469         for case in cases:
470             data = {}
471             if case.project_id.project_escalation_id:
472                 data['project_id'] = case.project_id.project_escalation_id.id
473                 if case.project_id.project_escalation_id.user_id:
474                     data['user_id'] = case.project_id.project_escalation_id.user_id.id
475                 if case.task_id:
476                     self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False})
477             else:
478                 raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
479             self.case_set(cr, uid, ids, 'draft', data, context=context)
480             self.case_escalate_send_note(cr, uid, [case.id], context=context)
481         return True
482
483     # -------------------------------------------------------
484     # Mail gateway
485     # -------------------------------------------------------
486
487     def message_new(self, cr, uid, msg, custom_values=None, context=None):
488         """ Overrides mail_thread message_new that is called by the mailgateway
489             through message_process.
490             This override updates the document according to the email.
491         """
492         if custom_values is None: custom_values = {}
493         if context is None: context = {}
494         context['state_to'] = 'draft'
495
496         desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
497
498         custom_values.update({
499             'name':  msg.get('subject') or _("No Subject"),
500             'description': desc,
501             'email_from': msg.get('from'),
502             'email_cc': msg.get('cc'),
503             'user_id': False,
504         })
505         if  msg.get('priority'):
506             custom_values['priority'] =  msg.get('priority')
507
508         res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
509         # self.convert_to_bug(cr, uid, [res_id], context=context)
510         return res_id
511
512     def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
513         """ Overrides mail_thread message_update that is called by the mailgateway
514             through message_process.
515             This method updates the document according to the email.
516         """
517         if isinstance(ids, (str, int, long)):
518             ids = [ids]
519         if update_vals is None: update_vals = {}
520
521         # Update doc values according to the message
522         if msg.get('priority'):
523             update_vals['priority'] = msg.get('priority')
524         # Parse 'body' to find values to update
525         maps = {
526             'cost': 'planned_cost',
527             'revenue': 'planned_revenue',
528             'probability': 'probability',
529         }
530         for line in msg.get('body', '').split('\n'):
531             line = line.strip()
532             res = tools.misc.command_re.match(line)
533             if res and maps.get(res.group(1).lower(), False):
534                 key = maps.get(res.group(1).lower())
535                 update_vals[key] = res.group(2).lower()
536
537         return super(project_issue, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
538
539     # -------------------------------------------------------
540     # OpenChatter methods and notifications
541     # -------------------------------------------------------
542
543     def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
544         """ Override of the (void) default notification method. """
545         stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
546         return self.message_post(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % (stage_name), context=context)
547
548     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
549         """ Override of default prefix for notifications. """
550         return 'Project issue'
551
552     def convert_to_task_send_note(self, cr, uid, ids, context=None):
553         message = _("Project issue <b>converted</b> to task.")
554         return self.message_post(cr, uid, ids, body=message, context=context)
555
556     def create_send_note(self, cr, uid, ids, context=None):
557         message = _("Project issue <b>created</b>.")
558         return self.message_post(cr, uid, ids, body=message, subtype="project_issue.mt_issue_new", context=context)
559
560     def case_open_send_note(self, cr, uid, ids, context=None):
561         return self.message_post(cr, uid, ids, body=_("Issue <b>started</b>."), subtype="project_issue.mt_issue_started", context=context)
562
563     def case_escalate_send_note(self, cr, uid, ids, context=None):
564         for obj in self.browse(cr, uid, ids, context=context):
565             if obj.project_id:
566                 message = _("<b>escalated</b> to <em>'%s'</em>.") % (obj.project_id.name)
567                 obj.message_post(body=message)
568             else:
569                 message = _("<b>escalated</b>.")
570                 obj.message_post(body=message)
571         return True
572
573     def case_block_send_note(self, cr, uid, ids, context=None):
574         return self.message_post(cr, uid, ids, body=_("Issue <b>blocked</b>."), subtype="project_issue.mt_issue_blocked", context=context)
575     
576     def case_close_send_note(self, cr, uid, ids, context=None):
577         return self.message_post(cr, uid, ids, body=_("Project issue <b>closed</b>."), subtype="project_issue.mt_issue_closed", context=context)
578
579     def set_kanban_state_blocked(self, cr, uid, ids, context=None):
580         self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
581         self.case_block_send_note(cr, uid, ids, context=context)
582         return True
583
584     def set_kanban_state_normal(self, cr, uid, ids, context=None):
585         self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
586         self.case_open_send_note(cr, uid, ids, context=context)
587         return True
588
589     def set_kanban_state_done(self, cr, uid, ids, context=None):
590         self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
591         return False
592 project_issue()
593
594 class project(osv.osv):
595     _inherit = "project.project"
596
597     def _get_alias_models(self, cr, uid, context=None):
598         return [('project.task', "Tasks"), ("project.issue", "Issues")]
599
600     def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
601         res = dict.fromkeys(ids, 0)
602         issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)])
603         for issue in self.pool.get('project.issue').browse(cr, uid, issue_ids, context):
604             res[issue.project_id.id] += 1
605         return res
606
607     _columns = {
608         '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)]}),
609         'issue_count': fields.function(_issue_count, type='integer'),
610     }
611
612     def _check_escalation(self, cr, uid, ids, context=None):
613         project_obj = self.browse(cr, uid, ids[0], context=context)
614         if project_obj.project_escalation_id:
615             if project_obj.project_escalation_id.id == project_obj.id:
616                 return False
617         return True
618
619     _constraints = [
620         (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
621     ]
622
623 project()
624
625 class account_analytic_account(osv.osv):
626     _inherit = 'account.analytic.account'
627     _description = 'Analytic Account'
628
629     _columns = {
630         'use_issues' : fields.boolean('Issues', help="Check this field if this project manages issues"),
631     }
632
633     def on_change_template(self, cr, uid, ids, template_id, context=None):
634         res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
635         if template_id and 'value' in res:
636             template = self.browse(cr, uid, template_id, context=context)
637             res['value']['use_issues'] = template.use_issues
638         return res
639
640     def _trigger_project_creation(self, cr, uid, vals, context=None):
641         if context is None: context = {}
642         res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
643         return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
644
645 account_analytic_account()
646
647 class project_project(osv.osv):
648     _inherit = 'project.project'
649     _defaults = {
650         'use_issues': True
651     }
652
653 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: